diff --git a/.air.toml b/.air.toml
index d13f8c4f99..de97bd8b29 100644
--- a/.air.toml
+++ b/.air.toml
@@ -8,6 +8,15 @@ delay = 1000
include_ext = ["go", "tmpl"]
include_file = ["main.go"]
include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
-exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"]
+exclude_dir = [
+ "models/fixtures",
+ "models/migrations/fixtures",
+ "modules/avatar/identicon/testdata",
+ "modules/avatar/testdata",
+ "modules/git/tests",
+ "modules/migration/file_format_testdata",
+ "routers/private/tests",
+ "services/gitdiff/testdata",
+]
exclude_regex = ["_test.go$", "_gen.go$"]
stop_on_error = true
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 9e290fb6a5..d391cf78cf 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,14 +1,16 @@
{
"name": "Gitea DevContainer",
- "image": "mcr.microsoft.com/devcontainers/go:1.21-bullseye",
+ "image": "mcr.microsoft.com/devcontainers/go:1.22-bullseye",
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {
- "version":"20"
+ "version": "20"
},
"ghcr.io/devcontainers/features/git-lfs:1.1.0": {},
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
- "ghcr.io/devcontainers/features/python:1": {}
+ "ghcr.io/devcontainers/features/python:1": {
+ "version": "3.12"
+ }
},
"customizations": {
"vscode": {
@@ -22,7 +24,7 @@
"DavidAnson.vscode-markdownlint",
"Vue.volar",
"ms-azuretools.vscode-docker",
- "zixuanchen.vitest-explorer",
+ "vitest.explorer",
"qwtel.sqlite-viewer",
"GitHub.vscode-pull-request-github"
]
diff --git a/.dockerignore b/.dockerignore
index 80cbeb040c..b696e1603c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -14,7 +14,7 @@ _test
# MS VSCode
.vscode
-__debug_bin
+__debug_bin*
# Architecture specific extensions/prefixes
*.[568vq]
@@ -62,7 +62,6 @@ cpu.out
/data
/indexers
/log
-/public/img/avatar
/tests/integration/gitea-integration-*
/tests/integration/indexers-*
/tests/e2e/gitea-e2e-*
@@ -78,7 +77,7 @@ cpu.out
/public/assets/js
/public/assets/css
/public/assets/fonts
-/public/assets/img/webpack
+/public/assets/img/avatar
/vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*
@@ -96,6 +95,9 @@ cpu.out
/.air
/.go-licenses
+# Files and folders that were previously generated
+/public/assets/img/webpack
+
# Snapcraft
snap/.snapcraft/
parts/
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index fc6f38ec53..5fd0a245f2 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -3,6 +3,7 @@ reportUnusedDisableDirectives: true
ignorePatterns:
- /web_src/js/vendor
+ - /web_src/fomantic
parserOptions:
sourceType: module
@@ -12,6 +13,7 @@ plugins:
- "@eslint-community/eslint-plugin-eslint-comments"
- "@stylistic/eslint-plugin-js"
- eslint-plugin-array-func
+ - eslint-plugin-github
- eslint-plugin-i
- eslint-plugin-jquery
- eslint-plugin-no-jquery
@@ -41,10 +43,6 @@ overrides:
worker: true
rules:
no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
- - files: ["build/generate-images.js"]
- rules:
- i/no-unresolved: [0]
- i/no-extraneous-dependencies: [0]
- files: ["*.config.*"]
rules:
i/no-unused-modules: [0]
@@ -122,7 +120,7 @@ rules:
"@stylistic/js/arrow-spacing": [2, {before: true, after: true}]
"@stylistic/js/block-spacing": [0]
"@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}]
- "@stylistic/js/comma-dangle": [2, only-multiline]
+ "@stylistic/js/comma-dangle": [2, always-multiline]
"@stylistic/js/comma-spacing": [2, {before: false, after: true}]
"@stylistic/js/comma-style": [2, last]
"@stylistic/js/computed-property-spacing": [2, never]
@@ -170,7 +168,7 @@ rules:
"@stylistic/js/semi-spacing": [2, {before: false, after: true}]
"@stylistic/js/semi-style": [2, last]
"@stylistic/js/space-before-blocks": [2, always]
- "@stylistic/js/space-before-function-paren": [0]
+ "@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}]
"@stylistic/js/space-in-parens": [2, never]
"@stylistic/js/space-infix-ops": [2]
"@stylistic/js/space-unary-ops": [2]
@@ -209,6 +207,29 @@ rules:
func-names: [0]
func-style: [0]
getter-return: [2]
+ github/a11y-aria-label-is-well-formatted: [0]
+ github/a11y-no-title-attribute: [0]
+ github/a11y-no-visually-hidden-interactive-element: [0]
+ github/a11y-role-supports-aria-props: [0]
+ github/a11y-svg-has-accessible-name: [0]
+ github/array-foreach: [0]
+ github/async-currenttarget: [2]
+ github/async-preventdefault: [2]
+ github/authenticity-token: [0]
+ github/get-attribute: [0]
+ github/js-class-name: [0]
+ github/no-blur: [0]
+ github/no-d-none: [0]
+ github/no-dataset: [2]
+ github/no-dynamic-script-tag: [2]
+ github/no-implicit-buggy-globals: [2]
+ github/no-inner-html: [0]
+ github/no-innerText: [2]
+ github/no-then: [2]
+ github/no-useless-passive: [2]
+ github/prefer-observers: [2]
+ github/require-passive-events: [2]
+ github/unescaped-html-literal: [0]
grouped-accessor-pairs: [2]
guard-for-in: [0]
id-blacklist: [0]
@@ -259,20 +280,20 @@ rules:
i/unambiguous: [0]
init-declarations: [0]
jquery/no-ajax-events: [2]
- jquery/no-ajax: [0]
+ jquery/no-ajax: [2]
jquery/no-animate: [2]
- jquery/no-attr: [0]
+ jquery/no-attr: [2]
jquery/no-bind: [2]
jquery/no-class: [0]
jquery/no-clone: [2]
jquery/no-closest: [0]
- jquery/no-css: [0]
+ jquery/no-css: [2]
jquery/no-data: [0]
jquery/no-deferred: [2]
jquery/no-delegate: [2]
jquery/no-each: [0]
jquery/no-extend: [2]
- jquery/no-fade: [0]
+ jquery/no-fade: [2]
jquery/no-filter: [0]
jquery/no-find: [0]
jquery/no-global-eval: [2]
@@ -283,15 +304,15 @@ rules:
jquery/no-in-array: [2]
jquery/no-is-array: [2]
jquery/no-is-function: [2]
- jquery/no-is: [0]
+ jquery/no-is: [2]
jquery/no-load: [2]
- jquery/no-map: [0]
+ jquery/no-map: [2]
jquery/no-merge: [2]
jquery/no-param: [2]
jquery/no-parent: [0]
jquery/no-parents: [0]
jquery/no-parse-html: [2]
- jquery/no-prop: [0]
+ jquery/no-prop: [2]
jquery/no-proxy: [2]
jquery/no-ready: [2]
jquery/no-serialize: [2]
@@ -372,12 +393,12 @@ rules:
no-irregular-whitespace: [2]
no-iterator: [2]
no-jquery/no-ajax-events: [2]
- no-jquery/no-ajax: [0]
+ no-jquery/no-ajax: [2]
no-jquery/no-and-self: [2]
no-jquery/no-animate-toggle: [2]
no-jquery/no-animate: [2]
- no-jquery/no-append-html: [0]
- no-jquery/no-attr: [0]
+ no-jquery/no-append-html: [2]
+ no-jquery/no-attr: [2]
no-jquery/no-bind: [2]
no-jquery/no-box-model: [2]
no-jquery/no-browser: [2]
@@ -389,7 +410,7 @@ rules:
no-jquery/no-constructor-attributes: [2]
no-jquery/no-contains: [2]
no-jquery/no-context-prop: [2]
- no-jquery/no-css: [0]
+ no-jquery/no-css: [2]
no-jquery/no-data: [0]
no-jquery/no-deferred: [2]
no-jquery/no-delegate: [2]
@@ -420,14 +441,14 @@ rules:
no-jquery/no-is-numeric: [2]
no-jquery/no-is-plain-object: [2]
no-jquery/no-is-window: [2]
- no-jquery/no-is: [0]
+ no-jquery/no-is: [2]
no-jquery/no-jquery-constructor: [0]
no-jquery/no-live: [2]
no-jquery/no-load-shorthand: [2]
no-jquery/no-load: [2]
no-jquery/no-map-collection: [0]
no-jquery/no-map-util: [2]
- no-jquery/no-map: [0]
+ no-jquery/no-map: [2]
no-jquery/no-merge: [2]
no-jquery/no-node-name: [2]
no-jquery/no-noop: [2]
@@ -442,7 +463,7 @@ rules:
no-jquery/no-parse-html: [2]
no-jquery/no-parse-json: [2]
no-jquery/no-parse-xml: [2]
- no-jquery/no-prop: [0]
+ no-jquery/no-prop: [2]
no-jquery/no-proxy: [2]
no-jquery/no-ready-shorthand: [2]
no-jquery/no-ready: [2]
@@ -463,7 +484,7 @@ rules:
no-jquery/no-visibility: [2]
no-jquery/no-when: [2]
no-jquery/no-wrap: [2]
- no-jquery/variable-pattern: [0]
+ no-jquery/variable-pattern: [2]
no-label-var: [2]
no-labels: [0] # handled by no-restricted-syntax
no-lone-blocks: [2]
@@ -516,7 +537,7 @@ rules:
no-underscore-dangle: [0]
no-unexpected-multiline: [2]
no-unmodified-loop-condition: [2]
- no-unneeded-ternary: [0]
+ no-unneeded-ternary: [2]
no-unreachable-loop: [2]
no-unreachable: [2]
no-unsafe-finally: [2]
@@ -558,7 +579,6 @@ rules:
prefer-rest-params: [2]
prefer-spread: [2]
prefer-template: [2]
- quotes: [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
radix: [2, as-needed]
regexp/confusing-quantifier: [2]
regexp/control-character-escape: [2]
@@ -696,12 +716,14 @@ rules:
unicorn/import-style: [0]
unicorn/new-for-builtins: [2]
unicorn/no-abusive-eslint-disable: [0]
+ unicorn/no-anonymous-default-export: [0]
unicorn/no-array-callback-reference: [0]
unicorn/no-array-for-each: [2]
unicorn/no-array-method-this-argument: [2]
unicorn/no-array-push-push: [2]
unicorn/no-array-reduce: [2]
unicorn/no-await-expression-member: [0]
+ unicorn/no-await-in-promise-methods: [2]
unicorn/no-console-spaces: [0]
unicorn/no-document-cookie: [2]
unicorn/no-empty-file: [2]
@@ -718,6 +740,7 @@ rules:
unicorn/no-null: [0]
unicorn/no-object-as-default-parameter: [0]
unicorn/no-process-exit: [0]
+ unicorn/no-single-promise-in-promise-methods: [2]
unicorn/no-static-only-class: [2]
unicorn/no-thenable: [2]
unicorn/no-this-assignment: [2]
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 624a2d97db..1447a6ea32 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1 @@
open_collective: gitea
-custom: https://www.bountysource.com/teams/gitea
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 8a5ab26975..d1b4d00d80 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,36 +1,77 @@
modifies/docs:
- - "**/*.md"
- - "docs/**"
-
-modifies/frontend:
- - "web_src/**/*"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.md"
+ - "docs/**"
modifies/templates:
- - all: ["templates/**", "!templates/swagger/v1_json.tmpl"]
+ - changed-files:
+ - all-globs-to-any-file:
+ - "templates/**"
+ - "!templates/swagger/v1_json.tmpl"
modifies/api:
- - "routers/api/**"
- - "templates/swagger/v1_json.tmpl"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "routers/api/**"
+ - "templates/swagger/v1_json.tmpl"
modifies/cli:
- - "cmd/**"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "cmd/**"
modifies/translation:
- - "options/locale/*.ini"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "options/locale/*.ini"
modifies/migrations:
- - "models/migrations/**/*"
+ - changed-files:
+ - any-glob-to-any-file:
+ - "models/migrations/**"
modifies/internal:
- - "Makefile"
- - "Dockerfile"
- - "Dockerfile.rootless"
- - "docker/**"
- - "webpack.config.js"
- - ".eslintrc.yaml"
- - ".golangci.yml"
- - ".markdownlint.yaml"
- - ".spectral.yaml"
- - ".stylelintrc.yaml"
- - ".yamllint.yaml"
- - ".github/**"
+ - changed-files:
+ - any-glob-to-any-file:
+ - ".air.toml"
+ - "Makefile"
+ - "Dockerfile"
+ - "Dockerfile.rootless"
+ - ".dockerignore"
+ - "docker/**"
+ - ".editorconfig"
+ - ".eslintrc.yaml"
+ - ".golangci.yml"
+ - ".gitpod.yml"
+ - ".markdownlint.yaml"
+ - ".spectral.yaml"
+ - "stylelint.config.js"
+ - ".yamllint.yaml"
+ - ".github/**"
+ - ".gitea/"
+ - ".devcontainer/**"
+ - "build.go"
+ - "build/**"
+ - "contrib/**"
+
+modifies/dependencies:
+ - changed-files:
+ - any-glob-to-any-file:
+ - "package.json"
+ - "package-lock.json"
+ - "pyproject.toml"
+ - "poetry.lock"
+ - "go.mod"
+ - "go.sum"
+
+modifies/go:
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.go"
+
+modifies/js:
+ - changed-files:
+ - any-glob-to-any-file:
+ - "**/*.js"
+ - "**/*.vue"
diff --git a/.github/workflows/cron-lock.yml b/.github/workflows/cron-lock.yml
deleted file mode 100644
index 746ec49bc6..0000000000
--- a/.github/workflows/cron-lock.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: cron-lock
-
-on:
- schedule:
- - cron: "0 0 * * *" # every day at 00:00 UTC
- workflow_dispatch:
-
-permissions:
- issues: write
- pull-requests: write
-
-concurrency:
- group: lock
-
-jobs:
- action:
- runs-on: ubuntu-latest
- if: github.repository == 'go-gitea/gitea'
- steps:
- - uses: dessant/lock-threads@v5
- with:
- issue-inactive-days: 45
diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml
index 390aae7c07..f1b51debf1 100644
--- a/.github/workflows/cron-translations.yml
+++ b/.github/workflows/cron-translations.yml
@@ -11,14 +11,19 @@ jobs:
if: github.repository == 'go-gitea/gitea'
steps:
- uses: actions/checkout@v4
- - name: download from crowdin
- uses: docker://jonasfranz/crowdin
+ - uses: crowdin/github-action@v1
+ with:
+ upload_sources: true
+ upload_translations: false
+ download_sources: false
+ download_translations: true
+ push_translations: false
+ push_sources: false
+ create_pull_request: false
+ config: crowdin.yml
env:
+ CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
- PLUGIN_DOWNLOAD: true
- PLUGIN_EXPORT_DIR: options/locale/
- PLUGIN_IGNORE_BRANCH: true
- PLUGIN_PROJECT_IDENTIFIER: gitea
- name: update locales
run: ./build/update-locales.sh
- name: push translations to repo
@@ -31,19 +36,3 @@ jobs:
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "git@github.com:go-gitea/gitea.git"
ssh_key: ${{ secrets.DEPLOY_KEY }}
- crowdin-push:
- runs-on: ubuntu-latest
- if: github.repository == 'go-gitea/gitea'
- steps:
- - uses: actions/checkout@v4
- - name: push translations to crowdin
- uses: docker://jonasfranz/crowdin
- env:
- CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }}
- PLUGIN_UPLOAD: true
- PLUGIN_EXPORT_DIR: options/locale/
- PLUGIN_IGNORE_BRANCH: true
- PLUGIN_PROJECT_IDENTIFIER: gitea
- PLUGIN_FILES: |
- locale_en-US.ini: options/locale/locale_en-US.ini
- PLUGIN_BRANCH: main
diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index c909f78597..9a609e0551 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -48,6 +48,7 @@ jobs:
- "Makefile"
- ".golangci.yml"
- ".editorconfig"
+ - "options/locale/locale_en-US.ini"
frontend:
- "**/*.js"
@@ -57,7 +58,7 @@ jobs:
- "package-lock.json"
- "Makefile"
- ".eslintrc.yaml"
- - ".stylelintrc.yaml"
+ - "stylelint.config.js"
- ".npmrc"
docs:
@@ -72,6 +73,7 @@ jobs:
- "Makefile"
templates:
+ - "tools/lint-templates-*.js"
- "templates/**/*.tmpl"
- "pyproject.toml"
- "poetry.lock"
diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 0472d9a9f0..99a69ab174 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -32,11 +32,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
- run: pip install poetry
- run: make deps-py
+ - run: make deps-frontend
- run: make lint-templates
lint-yaml:
@@ -45,9 +49,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- run: pip install poetry
- run: make deps-py
- run: make lint-yaml
@@ -64,6 +68,18 @@ jobs:
- run: make deps-frontend
- run: make lint-swagger
+ lint-spell:
+ if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
+ needs: files-changed
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ check-latest: true
+ - run: make lint-spell
+
lint-go-windows:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index a3886bf618..61c0391509 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -49,7 +49,10 @@ jobs:
- run: make backend
env:
TAGS: bindata
- - run: make test-pgsql-migration test-pgsql
+ - name: run migration tests
+ run: make test-pgsql-migration
+ - name: run tests
+ run: make test-pgsql
timeout-minutes: 50
env:
TAGS: bindata gogit
@@ -72,7 +75,10 @@ jobs:
- run: make backend
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
- - run: make test-sqlite-migration test-sqlite
+ - name: run migration tests
+ run: make test-sqlite-migration
+ - name: run tests
+ run: make test-sqlite
timeout-minutes: 50
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
@@ -175,8 +181,10 @@ jobs:
- run: make backend
env:
TAGS: bindata
+ - name: run migration tests
+ run: make test-mysql-migration
- name: run tests
- run: make test-mysql-migration integration-test-coverage
+ run: make integration-test-coverage
env:
TAGS: bindata
RACE_ENABLED: true
@@ -208,7 +216,9 @@ jobs:
- run: make backend
env:
TAGS: bindata
- - run: make test-mssql-migration test-mssql
+ - run: make test-mssql-migration
+ - name: run tests
+ run: make test-mssql
timeout-minutes: 50
env:
TAGS: bindata
diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml
index edd2f6d16e..812819b599 100644
--- a/.github/workflows/pull-labeler.yml
+++ b/.github/workflows/pull-labeler.yml
@@ -9,12 +9,12 @@ concurrency:
cancel-in-progress: true
jobs:
- label:
+ labeler:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- - uses: actions/labeler@v4
+ - uses: actions/labeler@v5
with:
- dot: true
+ sync-labels: true
diff --git a/.gitignore b/.gitignore
index 814d910315..46c8b9b49c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,7 +15,7 @@ _test
# MS VSCode
.vscode
-__debug_bin
+__debug_bin*
*.cgo1.go
*.cgo2.c
@@ -58,7 +58,7 @@ cpu.out
/data
/indexers
/log
-/public/img/avatar
+/public/assets/img/avatar
/tests/integration/gitea-integration-*
/tests/integration/indexers-*
/tests/e2e/gitea-e2e-*
@@ -77,7 +77,6 @@ cpu.out
/public/assets/css
/public/assets/fonts
/public/assets/licenses.txt
-/public/assets/img/webpack
/vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*
@@ -95,6 +94,9 @@ cpu.out
/.air
/.go-licenses
+# Files and folders that were previously generated
+/public/assets/img/webpack
+
# Snapcraft
/gitea_a*.txt
snap/.snapcraft/
diff --git a/.gitpod.yml b/.gitpod.yml
index 35b22c45ae..f573d55a76 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -10,10 +10,19 @@ tasks:
- name: Run backend
command: |
gp sync-await setup
- if [ ! -f custom/conf/app.ini ]
- then
+
+ # Get the URL and extract the domain
+ url=$(gp url 3000)
+ domain=$(echo $url | awk -F[/:] '{print $4}')
+
+ if [ -f custom/conf/app.ini ]; then
+ sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini
+ sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini
+ sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini
+ sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini
+ else
mkdir -p custom/conf/
- echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini
+ echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini
echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
fi
export TAGS="sqlite sqlite_unlock_notify"
@@ -33,7 +42,7 @@ vscode:
- DavidAnson.vscode-markdownlint
- Vue.volar
- ms-azuretools.vscode-docker
- - zixuanchen.vitest-explorer
+ - vitest.explorer
- qwtel.sqlite-viewer
- GitHub.vscode-pull-request-github
diff --git a/.golangci.yml b/.golangci.yml
index d6ce37f49a..5be2cefe44 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -30,10 +30,6 @@ linters:
run:
timeout: 10m
- skip-dirs:
- - node_modules
- - public
- - web_src
linters-settings:
stylecheck:
@@ -94,6 +90,7 @@ linters-settings:
issues:
max-issues-per-linter: 0
max-same-issues: 0
+ exclude-dirs: [node_modules, public, web_src]
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
diff --git a/.ignore b/.ignore
index 5c945ab981..5b96dabd38 100644
--- a/.ignore
+++ b/.ignore
@@ -4,6 +4,8 @@
/modules/options/bindata.go
/modules/public/bindata.go
/modules/templates/bindata.go
-/vendor
+/options/gitignore
+/options/license
/public/assets
+/vendor
node_modules
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index f740d1a4d6..b251ff796c 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -5,13 +5,11 @@ heading-increment: false
line-length: {code_blocks: false, tables: false, stern: true, line_length: -1}
no-alt-text: false
no-bare-urls: false
-no-blanks-blockquote: false
no-emphasis-as-heading: false
no-empty-links: false
no-hard-tabs: {code_blocks: false}
no-inline-html: false
no-space-in-code: false
no-space-in-emphasis: false
-no-trailing-punctuation: false
no-trailing-spaces: {br_spaces: 0}
single-h1: false
diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
deleted file mode 100644
index a44294ee76..0000000000
--- a/.stylelintrc.yaml
+++ /dev/null
@@ -1,221 +0,0 @@
-plugins:
- - stylelint-declaration-strict-value
- - stylelint-declaration-block-no-ignored-properties
- - "@stylistic/stylelint-plugin"
-
-ignoreFiles:
- - "**/*.go"
-
-overrides:
- - files: ["**/chroma/*", "**/codemirror/*", "**/standalone/*", "**/console.css", "font_i18n.css"]
- rules:
- scale-unlimited/declaration-strict-value: null
- - files: ["**/chroma/*", "**/codemirror/*"]
- rules:
- block-no-empty: null
- - files: ["**/*.vue"]
- customSyntax: postcss-html
-
-rules:
- "@stylistic/at-rule-name-case": null
- "@stylistic/at-rule-name-newline-after": null
- "@stylistic/at-rule-name-space-after": null
- "@stylistic/at-rule-semicolon-newline-after": null
- "@stylistic/at-rule-semicolon-space-before": null
- "@stylistic/block-closing-brace-empty-line-before": null
- "@stylistic/block-closing-brace-newline-after": null
- "@stylistic/block-closing-brace-newline-before": null
- "@stylistic/block-closing-brace-space-after": null
- "@stylistic/block-closing-brace-space-before": null
- "@stylistic/block-opening-brace-newline-after": null
- "@stylistic/block-opening-brace-newline-before": null
- "@stylistic/block-opening-brace-space-after": null
- "@stylistic/block-opening-brace-space-before": null
- "@stylistic/color-hex-case": lower
- "@stylistic/declaration-bang-space-after": never
- "@stylistic/declaration-bang-space-before": null
- "@stylistic/declaration-block-semicolon-newline-after": null
- "@stylistic/declaration-block-semicolon-newline-before": null
- "@stylistic/declaration-block-semicolon-space-after": null
- "@stylistic/declaration-block-semicolon-space-before": never
- "@stylistic/declaration-block-trailing-semicolon": null
- "@stylistic/declaration-colon-newline-after": null
- "@stylistic/declaration-colon-space-after": null
- "@stylistic/declaration-colon-space-before": never
- "@stylistic/function-comma-newline-after": null
- "@stylistic/function-comma-newline-before": null
- "@stylistic/function-comma-space-after": null
- "@stylistic/function-comma-space-before": null
- "@stylistic/function-max-empty-lines": 0
- "@stylistic/function-parentheses-newline-inside": never-multi-line
- "@stylistic/function-parentheses-space-inside": null
- "@stylistic/function-whitespace-after": null
- "@stylistic/indentation": 2
- "@stylistic/linebreaks": null
- "@stylistic/max-empty-lines": 1
- "@stylistic/max-line-length": null
- "@stylistic/media-feature-colon-space-after": null
- "@stylistic/media-feature-colon-space-before": never
- "@stylistic/media-feature-name-case": null
- "@stylistic/media-feature-parentheses-space-inside": null
- "@stylistic/media-feature-range-operator-space-after": always
- "@stylistic/media-feature-range-operator-space-before": always
- "@stylistic/media-query-list-comma-newline-after": null
- "@stylistic/media-query-list-comma-newline-before": null
- "@stylistic/media-query-list-comma-space-after": null
- "@stylistic/media-query-list-comma-space-before": null
- "@stylistic/no-empty-first-line": null
- "@stylistic/no-eol-whitespace": true
- "@stylistic/no-extra-semicolons": true
- "@stylistic/no-missing-end-of-source-newline": null
- "@stylistic/number-leading-zero": null
- "@stylistic/number-no-trailing-zeros": null
- "@stylistic/property-case": lower
- "@stylistic/selector-attribute-brackets-space-inside": null
- "@stylistic/selector-attribute-operator-space-after": null
- "@stylistic/selector-attribute-operator-space-before": null
- "@stylistic/selector-combinator-space-after": null
- "@stylistic/selector-combinator-space-before": null
- "@stylistic/selector-descendant-combinator-no-non-space": null
- "@stylistic/selector-list-comma-newline-after": null
- "@stylistic/selector-list-comma-newline-before": null
- "@stylistic/selector-list-comma-space-after": always-single-line
- "@stylistic/selector-list-comma-space-before": never-single-line
- "@stylistic/selector-max-empty-lines": 0
- "@stylistic/selector-pseudo-class-case": lower
- "@stylistic/selector-pseudo-class-parentheses-space-inside": never
- "@stylistic/selector-pseudo-element-case": lower
- "@stylistic/string-quotes": double
- "@stylistic/unicode-bom": null
- "@stylistic/unit-case": lower
- "@stylistic/value-list-comma-newline-after": null
- "@stylistic/value-list-comma-newline-before": null
- "@stylistic/value-list-comma-space-after": null
- "@stylistic/value-list-comma-space-before": null
- "@stylistic/value-list-max-empty-lines": 0
- alpha-value-notation: null
- annotation-no-unknown: true
- at-rule-allowed-list: null
- at-rule-disallowed-list: null
- at-rule-empty-line-before: null
- at-rule-no-unknown: true
- at-rule-no-vendor-prefix: true
- at-rule-property-required-list: null
- block-no-empty: true
- color-function-notation: null
- color-hex-alpha: null
- color-hex-length: null
- color-named: null
- color-no-hex: null
- color-no-invalid-hex: true
- comment-empty-line-before: null
- comment-no-empty: true
- comment-pattern: null
- comment-whitespace-inside: null
- comment-word-disallowed-list: null
- custom-media-pattern: null
- custom-property-empty-line-before: null
- custom-property-no-missing-var-function: true
- custom-property-pattern: null
- declaration-block-no-duplicate-custom-properties: true
- declaration-block-no-duplicate-properties: [true, {ignore: [consecutive-duplicates-with-different-values]}]
- declaration-block-no-redundant-longhand-properties: null
- declaration-block-no-shorthand-property-overrides: null
- declaration-block-single-line-max-declarations: null
- declaration-empty-line-before: null
- declaration-no-important: null
- declaration-property-max-values: null
- declaration-property-unit-allowed-list: null
- declaration-property-unit-disallowed-list: {line-height: [em]}
- declaration-property-value-allowed-list: null
- declaration-property-value-disallowed-list: null
- declaration-property-value-no-unknown: true
- font-family-name-quotes: always-where-recommended
- font-family-no-duplicate-names: true
- font-family-no-missing-generic-family-keyword: true
- font-weight-notation: null
- function-allowed-list: null
- function-calc-no-unspaced-operator: true
- function-disallowed-list: null
- function-linear-gradient-no-nonstandard-direction: true
- function-name-case: lower
- function-no-unknown: null
- function-url-no-scheme-relative: null
- function-url-quotes: always
- function-url-scheme-allowed-list: null
- function-url-scheme-disallowed-list: null
- hue-degree-notation: null
- import-notation: string
- keyframe-block-no-duplicate-selectors: true
- keyframe-declaration-no-important: true
- keyframe-selector-notation: null
- keyframes-name-pattern: null
- length-zero-no-unit: [true, ignore: [custom-properties], ignoreFunctions: [var]]
- max-nesting-depth: null
- media-feature-name-allowed-list: null
- media-feature-name-disallowed-list: null
- media-feature-name-no-unknown: true
- media-feature-name-no-vendor-prefix: true
- media-feature-name-unit-allowed-list: null
- media-feature-name-value-allowed-list: null
- media-feature-name-value-no-unknown: true
- media-feature-range-notation: null
- media-query-no-invalid: true
- named-grid-areas-no-invalid: true
- no-descending-specificity: null
- no-duplicate-at-import-rules: true
- no-duplicate-selectors: true
- no-empty-source: true
- no-invalid-double-slash-comments: true
- no-invalid-position-at-import-rule: null
- no-irregular-whitespace: true
- no-unknown-animations: null
- no-unknown-custom-properties: null
- number-max-precision: null
- plugin/declaration-block-no-ignored-properties: true
- property-allowed-list: null
- property-disallowed-list: null
- property-no-unknown: true
- property-no-vendor-prefix: null
- rule-empty-line-before: null
- rule-selector-property-disallowed-list: null
- scale-unlimited/declaration-strict-value: [[/color$/, font-weight], {ignoreValues: /^(inherit|transparent|unset|initial|currentcolor|none)$/, ignoreFunctions: false, disableFix: true, expandShorthand: true}]
- selector-attribute-name-disallowed-list: null
- selector-attribute-operator-allowed-list: null
- selector-attribute-operator-disallowed-list: null
- selector-attribute-quotes: always
- selector-class-pattern: null
- selector-combinator-allowed-list: null
- selector-combinator-disallowed-list: null
- selector-disallowed-list: null
- selector-id-pattern: null
- selector-max-attribute: null
- selector-max-class: null
- selector-max-combinators: null
- selector-max-compound-selectors: null
- selector-max-id: null
- selector-max-pseudo-class: null
- selector-max-specificity: null
- selector-max-type: null
- selector-max-universal: null
- selector-nested-pattern: null
- selector-no-qualifying-type: null
- selector-no-vendor-prefix: true
- selector-not-notation: null
- selector-pseudo-class-allowed-list: null
- selector-pseudo-class-disallowed-list: null
- selector-pseudo-class-no-unknown: true
- selector-pseudo-element-allowed-list: null
- selector-pseudo-element-colon-notation: double
- selector-pseudo-element-disallowed-list: null
- selector-pseudo-element-no-unknown: true
- selector-type-case: lower
- selector-type-no-unknown: [true, {ignore: [custom-elements]}]
- shorthand-property-no-redundant-values: true
- string-no-newline: true
- time-min-milliseconds: null
- unit-allowed-list: null
- unit-disallowed-list: null
- unit-no-unknown: true
- value-keyword-case: null
- value-no-vendor-prefix: [true, {ignoreValues: [box, inline-box]}]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae87638f1c..e119d0bec0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,240 @@ 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
been added to each release, please refer to the [blog](https://blog.gitea.com).
+## [1.21.6](https://github.com/go-gitea/gitea/releases/tag/v1.21.6) - 2024-02-22
+
+* SECURITY
+ * Fix XSS vulnerabilities (#29336)
+ * Use general token signing secret (#29205) (#29325)
+* ENHANCEMENTS
+ * Refactor git version functions and check compatibility (#29155) (#29157)
+ * Improve user experience for outdated comments (#29050) (#29086)
+ * Hide code links on release page if user cannot read code (#29064) (#29066)
+ * Wrap contained tags and branches again (#29021) (#29026)
+ * Fix incorrect button CSS usages (#29015) (#29023)
+ * Strip trailing newline in markdown code copy (#29019) (#29022)
+ * Implement some action notifier functions (#29173) (#29308)
+ * Load outdated comments when (un)resolving conversation on PR timeline (#29203) (#29221)
+* BUGFIXES
+ * Refactor issue template parsing and fix API endpoint (#29069) (#29140)
+ * Fix swift packages not resolving (#29095) (#29102)
+ * Remove SSH workaround (#27893) (#29332)
+ * Only log error when tag sync fails (#29295) (#29327)
+ * Fix SSPI user creation (#28948) (#29323)
+ * Improve the `issue_comment` workflow trigger event (#29277) (#29322)
+ * Discard unread data of `git cat-file` (#29297) (#29310)
+ * Fix error display when merging PRs (#29288) (#29309)
+ * Prevent double use of `git cat-file` session. (#29298) (#29301)
+ * Fix missing link on outgoing new release notifications (#29079) (#29300)
+ * Fix debian InRelease Acquire-By-Hash newline (#29204) (#29299)
+ * Always write proc-receive hook for all git versions (#29287) (#29291)
+ * Do not show delete button when time tracker is disabled (#29257) (#29279)
+ * Workaround to clean up old reviews on creating a new one (#28554) (#29264)
+ * Fix bug when the linked account was disactived and list the linked accounts (#29263)
+ * Do not use lower tag names to find releases/tags (#29261) (#29262)
+ * Fix missed edit issues event for actions (#29237) (#29251)
+ * Only delete scheduled workflows when needed (#29091) (#29235)
+ * Make submit event code work with both jQuery event and native event (#29223) (#29234)
+ * Fix push to create with capitalize repo name (#29090) (#29206)
+ * Use ghost user if user was not found (#29161) (#29169)
+ * Dont load Review if Comment is CommentTypeReviewRequest (#28551) (#29160)
+ * Refactor parseSignatureFromCommitLine (#29054) (#29108)
+ * Avoid showing unnecessary JS errors when there are elements with different origin on the page (#29081) (#29089)
+ * Fix gitea-origin-url with default ports (#29085) (#29088)
+ * Fix orgmode link resolving (#29024) (#29076)
+ * Fix Elasticsearh Request Entity Too Large #28117 (#29062) (#29075)
+ * Do not render empty comments (#29039) (#29049)
+ * Avoid sending update/delete release notice when it is draft (#29008) (#29025)
+ * Fix gitea-action user avatar broken on edited menu (#29190) (#29307)
+ * Disallow merge when required checked are missing (#29143) (#29268)
+ * Fix incorrect link to swift doc and swift package-registry login command (#29096) (#29103)
+ * Convert visibility to number (#29226) (#29244)
+* DOCS
+ * Remove outdated docs from some languages (#27530) (#29208)
+ * Fix typos in the documentation (#29048) (#29056)
+ * Explained where create issue/PR template (#29035)
+
+## [1.21.5](https://github.com/go-gitea/gitea/releases/tag/v1.21.5) - 2024-01-31
+
+* SECURITY
+ * Prevent anonymous container access if `RequireSignInView` is enabled (#28877) (#28882)
+ * Update go dependencies and fix go-git (#28893) (#28934)
+* BUGFIXES
+ * Revert "Speed up loading the dashboard on mysql/mariadb (#28546)" (#29006) (#29007)
+ * Fix an actions schedule bug (#28942) (#28999)
+ * Fix update enable_prune even if mirror_interval is not provided (#28905) (#28929)
+ * Fix uploaded artifacts should be overwritten (#28726) backport v1.21 (#28832)
+ * Preserve BOM in web editor (#28935) (#28959)
+ * Strip `/` from relative links (#28932) (#28952)
+ * Don't remove all mirror repository's releases when mirroring (#28817) (#28939)
+ * Implement `MigrateRepository` for the actions notifier (#28920) (#28923)
+ * Respect branch info for relative links (#28909) (#28922)
+ * Don't reload timeline page when (un)resolving or replying conversation (#28654) (#28917)
+ * Only migrate the first 255 chars of a Github issue title (#28902) (#28912)
+ * Fix sort bug on repository issues list (#28897) (#28901)
+ * Fix `DeleteCollaboration` transaction behaviour (#28886) (#28889)
+ * Fix schedule not trigger bug because matching full ref name with short ref name (#28874) (#28888)
+ * Fix migrate storage bug (#28830) (#28867)
+ * Fix archive creating LFS hooks and breaking pull requests (#28848) (#28851)
+ * Fix reverting a merge commit failing (#28794) (#28825)
+ * Upgrade xorm to v1.3.7 to fix a resource leak problem caused by Iterate (#28891) (#28895)
+ * Fix incorrect PostgreSQL connection string for Unix sockets (#28865) (#28870)
+* ENHANCEMENTS
+ * Make loading animation less aggressive (#28955) (#28956)
+ * Avoid duplicate JS error messages on UI (#28873) (#28881)
+ * Bump `@github/relative-time-element` to 4.3.1 (#28819) (#28826)
+* MISC
+ * Warn that `DISABLE_QUERY_AUTH_TOKEN` is false only if it's explicitly defined (#28783) (#28868)
+ * Remove duplicated checkinit on git module (#28824) (#28831)
+
+## [1.21.4](https://github.com/go-gitea/gitea/releases/tag/v1.21.4) - 2024-01-16
+
+* SECURITY
+ * Update github.com/cloudflare/circl (#28789) (#28790)
+ * Require token for GET subscription endpoint (#28765) (#28768)
+* BUGFIXES
+ * Use refname:strip-2 instead of refname:short when syncing tags (#28797) (#28811)
+ * Fix links in issue card (#28806) (#28807)
+ * Fix nil pointer panic when exec some gitea cli command (#28791) (#28795)
+ * Require token for GET subscription endpoint (#28765) (#28778)
+ * Fix button size in "attached header right" (#28770) (#28774)
+ * Fix `convert.ToTeams` on empty input (#28426) (#28767)
+ * Hide code related setting options in repository when code unit is disabled (#28631) (#28749)
+ * Fix incorrect URL for "Reference in New Issue" (#28716) (#28723)
+ * Fix panic when parsing empty pgsql host (#28708) (#28709)
+ * Upgrade xorm to new version which supported update join for all supported databases (#28590) (#28668)
+ * Fix alpine package files are not rebuilt (#28638) (#28665)
+ * Avoid cycle-redirecting user/login page (#28636) (#28658)
+ * Fix empty ref for cron workflow runs (#28640) (#28647)
+ * Remove unnecessary syncbranchToDB with tests (#28624) (#28629)
+ * Use known issue IID to generate new PR index number when migrating from GitLab (#28616) (#28618)
+ * Fix flex container width (#28603) (#28605)
+ * Fix the scroll behavior for emoji/mention list (#28597) (#28601)
+ * Fix wrong due date rendering in issue list page (#28588) (#28591)
+ * Fix `status_check_contexts` matching bug (#28582) (#28589)
+ * Fix 500 error of searching commits (#28576) (#28579)
+ * Use information from previous blame parts (#28572) (#28577)
+ * Update mermaid for 1.21 (#28571)
+ * Fix 405 method not allowed CORS / OIDC (#28583) (#28586) (#28587) (#28611)
+ * Fix `GetCommitStatuses` (#28787) (#28804)
+ * Forbid removing the last admin user (#28337) (#28793)
+ * Fix schedule tasks bugs (#28691) (#28780)
+ * Fix issue dependencies (#27736) (#28776)
+ * Fix system webhooks API bug (#28531) (#28666)
+ * Fix when private user following user, private user will not be counted in his own view (#28037) (#28792)
+ * Render code block in activity tab (#28816) (#28818)
+* ENHANCEMENTS
+ * Rework markup link rendering (#26745) (#28803)
+ * Modernize merge button (#28140) (#28786)
+ * Speed up loading the dashboard on mysql/mariadb (#28546) (#28784)
+ * Assign pull request to project during creation (#28227) (#28775)
+ * Show description as tooltip instead of title for labels (#28754) (#28766)
+ * Make template `DateTime` show proper tooltip (#28677) (#28683)
+ * Switch destination directory for apt signing keys (#28639) (#28642)
+ * Include heap pprof in diagnosis report to help debugging memory leaks (#28596) (#28599)
+* DOCS
+ * Suggest to use Type=simple for systemd service (#28717) (#28722)
+ * Extend description for ARTIFACT_RETENTION_DAYS (#28626) (#28630)
+* MISC
+ * Add -F to commit search to treat keywords as strings (#28744) (#28748)
+ * Add download attribute to release attachments (#28739) (#28740)
+ * Concatenate error in `checkIfPRContentChanged` (#28731) (#28737)
+ * Improve 1.21 document for Database Preparation (#28643) (#28644)
+
+## [1.21.3](https://github.com/go-gitea/gitea/releases/tag/v1.21.3) - 2023-12-21
+
+* SECURITY
+ * Update golang.org/x/crypto (#28519)
+* API
+ * chore(api): support ignore password if login source type is LDAP for creating user API (#28491) (#28525)
+ * Add endpoint for not implemented Docker auth (#28457) (#28462)
+* ENHANCEMENTS
+ * Add option to disable ambiguous unicode characters detection (#28454) (#28499)
+ * Refactor SSH clone URL generation code (#28421) (#28480)
+ * Polyfill SubmitEvent for PaleMoon (#28441) (#28478)
+* BUGFIXES
+ * Fix the issue ref rendering for wiki (#28556) (#28559)
+ * Fix duplicate ID when deleting repo (#28520) (#28528)
+ * Only check online runner when detecting matching runners in workflows (#28286) (#28512)
+ * Initalize stroage for orphaned repository doctor (#28487) (#28490)
+ * Fix possible nil pointer access (#28428) (#28440)
+ * Don't show unnecessary citation JS error on UI (#28433) (#28437)
+* DOCS
+ * Update actions document about comparsion as Github Actions (#28560) (#28564)
+ * Fix documents for "custom/public/assets/" (#28465) (#28467)
+* MISC
+ * Fix inperformant query on retrifing review from database. (#28552) (#28562)
+ * Improve the prompt for "ssh-keygen sign" (#28509) (#28510)
+ * Update docs for DISABLE_QUERY_AUTH_TOKEN (#28485) (#28488)
+ * Fix Chinese translation of config cheat sheet[API] (#28472) (#28473)
+ * Retry SSH key verification with additional CRLF if it failed (#28392) (#28464)
+
+## [1.21.2](https://github.com/go-gitea/gitea/releases/tag/v1.21.2) - 2023-12-12
+
+* SECURITY
+ * Rebuild with recently released golang version
+ * Fix missing check (#28406) (#28411)
+ * Do some missing checks (#28423) (#28432)
+* BUGFIXES
+ * Fix margin in server signed signature verification view (#28379) (#28381)
+ * Fix object does not exist error when checking citation file (#28314) (#28369)
+ * Use `filepath` instead of `path` to create SQLite3 database file (#28374) (#28378)
+ * Fix the runs will not be displayed bug when the main branch have no workflows but other branches have (#28359) (#28365)
+ * Handle repository.size column being NULL in migration v263 (#28336) (#28363)
+ * Convert git commit summary to valid UTF8. (#28356) (#28358)
+ * Fix migration panic due to an empty review comment diff (#28334) (#28362)
+ * Add `HEAD` support for rpm repo files (#28309) (#28360)
+ * Fix RPM/Debian signature key creation (#28352) (#28353)
+ * Keep profile tab when clicking on Language (#28320) (#28331)
+ * Fix missing issue search index update when changing status (#28325) (#28330)
+ * Fix wrong link in `protect_branch_name_pattern_desc` (#28313) (#28315)
+ * Read `previous` info from git blame (#28306) (#28310)
+ * Ignore "non-existing" errors when getDirectorySize calculates the size (#28276) (#28285)
+ * Use appSubUrl for OAuth2 callback URL tip (#28266) (#28275)
+ * Meilisearch: require all query terms to be matched (#28293) (#28296)
+ * Fix required error for token name (#28267) (#28284)
+ * Fix issue will be detected as pull request when checking `First-time contributor` (#28237) (#28271)
+ * Use full width for project boards (#28225) (#28245)
+ * Increase "version" when update the setting value to a same value as before (#28243) (#28244)
+ * Also sync DB branches on push if necessary (#28361) (#28403)
+ * Make gogit Repository.GetBranchNames consistent (#28348) (#28386)
+ * Recover from panic in cron task (#28409) (#28425)
+ * Deprecate query string auth tokens (#28390) (#28430)
+* ENHANCEMENTS
+ * Improve doctor cli behavior (#28422) (#28424)
+ * Fix margin in server signed signature verification view (#28379) (#28381)
+ * Refactor template empty checks (#28351) (#28354)
+ * Read `previous` info from git blame (#28306) (#28310)
+ * Use full width for project boards (#28225) (#28245)
+ * Enable system users search via the API (#28013) (#28018)
+
+## [1.21.1](https://github.com/go-gitea/gitea/releases/tag/v1.21.1) - 2023-11-26
+
+* SECURITY
+ * Fix comment permissions (#28213) (#28216)
+* BUGFIXES
+ * Fix delete-orphaned-repos (#28200) (#28202)
+ * Make CORS work for oauth2 handlers (#28184) (#28185)
+ * Fix missing buttons (#28179) (#28181)
+ * Fix no ActionTaskOutput table waring (#28149) (#28152)
+ * Fix empty action run title (#28113) (#28148)
+ * Use "is-loading" to avoid duplicate form submit for code comment (#28143) (#28147)
+ * Fix Matrix and MSTeams nil dereference (#28089) (#28105)
+ * Fix incorrect pgsql conn builder behavior (#28085) (#28098)
+ * Fix system config cache expiration timing (#28072) (#28090)
+ * Restricted users only see repos in orgs which their team was assigned to (#28025) (#28051)
+* API
+ * Fix permissions for Token DELETE endpoint to match GET and POST (#27610) (#28099)
+* ENHANCEMENTS
+ * Do not display search box when there's no packages yet (#28146) (#28159)
+ * Add missing `packages.cleanup.success` (#28129) (#28132)
+* DOCS
+ * Docs: Replace deprecated IS_TLS_ENABLED mailer setting in email setup (#28205) (#28208)
+ * Fix the description about the default setting for action in quick start document (#28160) (#28168)
+ * Add guide page to actions when there's no workflows (#28145) (#28153)
+* MISC
+ * Use full width for PR comparison (#28182) (#28186)
+
## [1.21.0](https://github.com/go-gitea/gitea/releases/tag/v1.21.0) - 2023-11-14
* BREAKING
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 96b02edd5b..5d20bc2589 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,6 +8,7 @@
- [How to report issues](#how-to-report-issues)
- [Types of issues](#types-of-issues)
- [Discuss your design before the implementation](#discuss-your-design-before-the-implementation)
+ - [Issue locking](#issue-locking)
- [Building Gitea](#building-gitea)
- [Dependencies](#dependencies)
- [Backend](#backend)
@@ -47,6 +48,7 @@
- [Release Cycle](#release-cycle)
- [Maintainers](#maintainers)
- [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc)
+ - [TOC election process](#toc-election-process)
- [Current TOC members](#current-toc-members)
- [Previous TOC/owners members](#previous-tocowners-members)
- [Governance Compensation](#governance-compensation)
@@ -102,6 +104,13 @@ the goals for the project and tools.
Pull requests should not be the place for architecture discussions.
+### Issue locking
+
+Commenting on closed or merged issues/PRs is strongly discouraged.
+Such comments will likely be overlooked as some maintainers may not view notifications on closed issues, thinking that the item is resolved.
+As such, commenting on closed/merged issues/PRs may be disabled prior to the scheduled auto-locking if a discussion starts or if unrelated comments are posted.
+If further discussion is needed, we encourage you to open a new issue instead and we recommend linking to the issue/PR in question for context.
+
## Building Gitea
See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea).
@@ -455,7 +464,7 @@ We assume in good faith that the information you provide is legally binding.
We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \
The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \
All the feature pull requests should be
-merged before feature freeze. And, during the frozen period, a corresponding
+merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding
release branch is open for fixes backported from main branch. Release candidates
are made during this period for user testing to
obtain a final version that is maintained in this branch.
@@ -486,36 +495,53 @@ if possible provide GPG signed commits.
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
https://help.github.com/articles/signing-commits-with-gpg/
+Furthermore, any account with write access (like bots and TOC members) **must** use 2FA.
+https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+
## Technical Oversight Committee (TOC)
-At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions would be elected as it has been over the past years, and the other three would consist of appointed members from the Gitea company.
+At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company.
https://blog.gitea.com/quarterly-23q1/
-When the new community members have been elected, the old members will give up ownership to the newly elected members. For security reasons, TOC members or any account with write access (like a bot) must use 2FA.
-https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
+### TOC election process
+
+Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company.
+A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election.
+If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate.
+
+The TOC is elected for one year, the TOC election happens yearly.
+After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat.
+If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat.
+Refusals result in the person with the next highest vote getting the same choice.
+As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat.
+
+If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration.
### Current TOC members
-- 2023-01-01 ~ 2023-12-31 - https://blog.gitea.com/quarterly-23q1/
+- 2024-01-01 ~ 2024-12-31
- Company
- [Jason Song](https://gitea.com/wolfogre)
- [Lunny Xiao](https://gitea.com/lunny)
-
-
-
-
- View this document in Chinese
-
-
-
-
-
- View this document in English
-
-
-
Gitea - Git with a cup of tea
+# Gitea
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Gitea - Git with a cup of tea
+# Gitea
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
"); err != nil { - return err - } - if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil { - return err - } - _, err = tmpBlock.WriteString("") + if int64(len(rawBytes)) <= maxSize { + return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock) + } + return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock) +} + +func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error { + _, err := tmpBlock.WriteString("
") + if err != nil { return err } - rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, bytes.NewReader(rawBytes)) + scan := bufio.NewScanner(input) + scan.Split(bufio.ScanRunes) + for scan.Scan() { + switch scan.Text() { + case `&`: + _, err = tmpBlock.WriteString("&") + case `'`: + _, err = tmpBlock.WriteString("'") // "'" is shorter than "'" and apos was not in HTML until HTML5. + case `<`: + _, err = tmpBlock.WriteString("<") + case `>`: + _, err = tmpBlock.WriteString(">") + case `"`: + _, err = tmpBlock.WriteString(""") // """ is shorter than """. + default: + _, err = tmpBlock.Write(scan.Bytes()) + } + if err != nil { + return err + } + } + if err = scan.Err(); err != nil { + return fmt.Errorf("fallbackRender scan: %w", err) + } + + _, err = tmpBlock.WriteString("") + if err != nil { + return err + } + return tmpBlock.Flush() +} + +func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error { + rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input) if err != nil { return err } diff --git a/modules/markup/csv/csv_test.go b/modules/markup/csv/csv_test.go index 8c07184b21..3d12be477c 100644 --- a/modules/markup/csv/csv_test.go +++ b/modules/markup/csv/csv_test.go @@ -4,6 +4,8 @@ package markup import ( + "bufio" + "bytes" "strings" "testing" @@ -29,4 +31,12 @@ func TestRenderCSV(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, v, buf.String()) } + + t.Run("fallbackRender", func(t *testing.T) { + var buf bytes.Buffer + err := render.fallbackRender(strings.NewReader("1,\n2,"), bufio.NewWriter(&buf)) + assert.NoError(t, err) + want := "
1,<a>\n2,<b>" + assert.Equal(t, want, buf.String()) + }) } diff --git a/modules/markup/html.go b/modules/markup/html.go index 33dc1e9086..cef643bf18 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -53,38 +53,38 @@ var ( // shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) - // anySHA1Pattern splits url containing SHA into parts + // anyHashPattern splits url containing SHA into parts anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`) // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) - validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) + // fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..." + fullURLPattern = regexp.MustCompile(`^[a-z][-+\w]+:`) - // While this email regex is definitely not perfect and I'm sure you can come up - // with edge cases, it is still accepted by the CommonMark specification, as - // well as the HTML5 spec: + // emailRegex is definitely not perfect with edge cases, + // it is still accepted by the CommonMark specification, as well as the HTML5 spec: // http://spec.commonmark.org/0.28/#email-address // https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail) emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))") - // blackfriday extensions create IDs like fn:user-content-footnote + // blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) - // EmojiShortCodeRegex find emoji by alias like :smile: - EmojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) + // emojiShortCodeRegex find emoji by alias like :smile: + emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`) ) // CSS class for action keywords (e.g. "closes: #1") const keywordClass = "issue-keyword" -// IsLink reports whether link fits valid format. -func IsLink(link []byte) bool { - return validLinksPattern.Match(link) +// IsFullURLBytes reports whether link fits valid format. +func IsFullURLBytes(link []byte) bool { + return fullURLPattern.Match(link) } -func IsLinkStr(link string) bool { - return validLinksPattern.MatchString(link) +func IsFullURLString(link string) bool { + return fullURLPattern.MatchString(link) } // regexp for full links to issues/pulls @@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node) var defaultProcessors = []processor{ fullIssuePatternProcessor, comparePatternProcessor, + codePreviewPatternProcessor, fullHashPatternProcessor, shortLinkProcessor, linkProcessor, @@ -399,7 +400,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) { if attr.Key != "src" { continue } - if len(attr.Val) > 0 && !IsLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") { + if len(attr.Val) > 0 && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") { attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val) } attr.Val = camoHandleLink(attr.Val) @@ -609,7 +610,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { if ok && strings.Contains(mention, "/") { mentionOrgAndTeam := strings.Split(mention, "/") if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { - replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) + replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) node = node.NextSibling.NextSibling start = 0 continue @@ -620,7 +621,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { mentionedUsername := mention[1:] if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { - replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention")) + replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention")) node = node.NextSibling.NextSibling } else { node = node.NextSibling @@ -650,7 +651,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { if equalPos := strings.IndexByte(v, '='); equalPos == -1 { // There is no equal in this argument; this is a mandatory arg if props["name"] == "" { - if IsLinkStr(v) { + if IsFullURLString(v) { // If we clearly see it is a link, we save it so // But first we need to ensure, that if both mandatory args provided @@ -708,7 +709,8 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { name += tail image := false - switch ext := filepath.Ext(link); ext { + ext := filepath.Ext(link) + switch ext { // fast path: empty string, ignore case "": // leave image as false @@ -725,7 +727,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { DataAtom: atom.A, } childNode.Parent = linkNode - absoluteLink := IsLinkStr(link) + absoluteLink := IsFullURLString(link) if !absoluteLink { if image { link = strings.ReplaceAll(link, " ", "+") @@ -766,11 +768,26 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { } } else { if !absoluteLink { + var base string if ctx.IsWiki { - link = util.URLJoin(ctx.Links.WikiLink(), link) + switch ext { + case "": + // no file extension, create a regular wiki link + base = ctx.Links.WikiLink() + default: + // we have a file extension: + // return a regular wiki link if it's a renderable file (extension), + // raw link otherwise + if Type(link) != "" { + base = ctx.Links.WikiLink() + } else { + base = ctx.Links.WikiRawLink() + } + } } else { - link = util.URLJoin(ctx.Links.SrcLink(), link) + base = ctx.Links.SrcLink() } + link = util.URLJoin(base, link) } childNode.Type = html.TextNode childNode.Data = name @@ -804,7 +821,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { // indicate that in the text by appending (comment) if m[4] != -1 && m[5] != -1 { if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok { - text += " " + locale.Tr("repo.from_comment") + text += " " + locale.TrString("repo.from_comment") } else { text += " (comment)" } @@ -898,9 +915,9 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { path = "pulls" } if ref.Owner == "" { - link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue") + link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue") } else { - link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue") + link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue") } } @@ -939,7 +956,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { } reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - link := createLink(util.URLJoin(setting.AppSubURL, ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit") + link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit") replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) node = node.NextSibling.NextSibling @@ -1059,7 +1076,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { start := 0 next := node.NextSibling for node != nil && node != next && start < len(node.Data) { - m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:]) + m := emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:]) if m == nil { return } @@ -1166,7 +1183,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { continue } - link := util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash) + link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash) replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) start = 0 node = node.NextSibling.NextSibling diff --git a/modules/markup/html_codepreview.go b/modules/markup/html_codepreview.go new file mode 100644 index 0000000000..d9da24ea34 --- /dev/null +++ b/modules/markup/html_codepreview.go @@ -0,0 +1,92 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "html/template" + "net/url" + "regexp" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" + + "golang.org/x/net/html" +) + +// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20" +var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`) + +type RenderCodePreviewOptions struct { + FullURL string + OwnerName string + RepoName string + CommitID string + FilePath string + + LineStart, LineStop int +} + +func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) { + m := codePreviewPattern.FindStringSubmatchIndex(node.Data) + if m == nil { + return 0, 0, "", nil + } + + opts := RenderCodePreviewOptions{ + FullURL: node.Data[m[0]:m[1]], + OwnerName: node.Data[m[2]:m[3]], + RepoName: node.Data[m[4]:m[5]], + CommitID: node.Data[m[6]:m[7]], + FilePath: node.Data[m[8]:m[9]], + } + if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) { + return 0, 0, "", nil + } + u, err := url.Parse(opts.FilePath) + if err != nil { + return 0, 0, "", err + } + opts.FilePath = strings.TrimPrefix(u.Path, "/") + + lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-") + lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L")) + lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L")) + opts.LineStart, opts.LineStop = lineStart, lineStop + h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts) + return m[0], m[1], h, err +} + +func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { + for node != nil { + if node.Type != html.TextNode { + node = node.NextSibling + continue + } + urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node) + if err != nil || h == "" { + if err != nil { + log.Error("Unable to render code preview: %v", err) + } + node = node.NextSibling + continue + } + next := node.NextSibling + textBefore := node.Data[:urlPosStart] + textAfter := node.Data[urlPosEnd:] + // "textBefore" could be empty if there is only a URL in the text node, then an empty node (p, or li) will be left here. + // However, the empty node can't be simply removed, because: + // 1. the following processors will still try to access it (need to double-check undefined behaviors) + // 2. the new node is inserted as "
{TextBefore}
{TextAfter}" (the parent could also be "li") + // then it is resolved as: "{TextBefore}
{TextAfter}
", + // so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node. + node.Data = textBefore + node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next) + if textAfter != "" { + node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next) + } + node = next + } +} diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go new file mode 100644 index 0000000000..d33630d040 --- /dev/null +++ b/modules/markup/html_codepreview_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup_test + +import ( + "context" + "html/template" + "strings" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/markup" + + "github.com/stretchr/testify/assert" +) + +func TestRenderCodePreview(t *testing.T) { + markup.Init(&markup.ProcessorHelper{ + RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) { + return "http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20
`) +} diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 93ba9d7667..e313be7040 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -287,6 +287,7 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) { } func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { + ctx.Links.AbsolutePrefix = true if ctx.Links.Base == "" { ctx.Links.Base = TestRepoURL } diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 89ecfc036b..916e74fb62 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -43,7 +43,8 @@ func TestRender_Commits(t *testing.T) { Ctx: git.DefaultContext, RelativePath: ".md", Links: markup.Links{ - Base: markup.TestRepoURL, + AbsolutePrefix: true, + Base: markup.TestRepoURL, }, Metas: localMetas, }, input) @@ -96,7 +97,8 @@ func TestRender_CrossReferences(t *testing.T) { Ctx: git.DefaultContext, RelativePath: "a.md", Links: markup.Links{ - Base: setting.AppSubURL, + AbsolutePrefix: true, + Base: setting.AppSubURL, }, Metas: localMetas, }, input) @@ -204,6 +206,15 @@ func TestRender_links(t *testing.T) { test( "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download", `magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download
`) + test( + `[link](https://example.com)`, + ``) + test( + `[link](mailto:test@example.com)`, + ``) + test( + `[link](javascript:xss)`, + `link
`) // Test that should *not* be turned into URL test( @@ -388,7 +399,7 @@ func TestRender_ShortLinks(t *testing.T) { }, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) buffer, err = markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ @@ -398,7 +409,7 @@ func TestRender_ShortLinks(t *testing.T) { IsWiki: true, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) } mediatree := util.URLJoin(markup.TestRepoURL, "media", "master") @@ -416,6 +427,10 @@ func TestRender_ShortLinks(t *testing.T) { otherImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+Other.jpg") encodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+%23.jpg") notencodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "some", "path", "Link+#.jpg") + renderableFileURL := util.URLJoin(tree, "markdown_file.md") + renderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "markdown_file.md") + unrenderableFileURL := util.URLJoin(tree, "file.zip") + unrenderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "file.zip") favicon := "http://google.com/favicon.ico" test( @@ -470,6 +485,14 @@ func TestRender_ShortLinks(t *testing.T) { "[[Link]] [[Other Link]] [[Link?]]", ``, ``) + test( + "[[markdown_file.md]]", + ``, + ``) + test( + "[[file.zip]]", + ``, + ``) test( "[[Link #.jpg]]", ``, @@ -501,7 +524,7 @@ func TestRender_RelativeImages(t *testing.T) { Metas: localMetas, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) buffer, err = markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ @@ -511,7 +534,7 @@ func TestRender_RelativeImages(t *testing.T) { IsWiki: true, }, input) assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) + assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer))) } rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw") @@ -579,7 +602,8 @@ func TestPostProcess_RenderDocument(t *testing.T) { err := markup.PostProcess(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ - Base: "https://example.com", + AbsolutePrefix: true, + Base: "https://example.com", }, Metas: localMetas, }, strings.NewReader(input), &res) @@ -673,3 +697,9 @@ func TestIssue18471(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "783b039...da951ce
", res.String())
}
+
+func TestIsFullURL(t *testing.T) {
+ assert.True(t, markup.IsFullURLString("https://example.com"))
+ assert.True(t, markup.IsFullURLString("mailto:test@example.com"))
+ assert.False(t, markup.IsFullURLString("/foo:bar"))
+}
diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go
index 3e6e291ab2..624c35d945 100644
--- a/modules/markup/markdown/ast.go
+++ b/modules/markup/markdown/ast.go
@@ -175,19 +175,7 @@ func NewColorPreview(color []byte) *ColorPreview {
}
}
-// IsColorPreview returns true if the given node implements the ColorPreview interface,
-// otherwise false.
-func IsColorPreview(node ast.Node) bool {
- _, ok := node.(*ColorPreview)
- return ok
-}
-
-const (
- AttentionNote string = "Note"
- AttentionWarning string = "Warning"
-)
-
-// Attention is an inline for a color preview
+// Attention is an inline for an attention
type Attention struct {
ast.BaseInline
AttentionType string
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 178e3d2fdd..b8b3aeaab0 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -4,19 +4,14 @@
package markdown
import (
- "bytes"
"fmt"
"regexp"
"strings"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/common"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/svg"
- giteautil "code.gitea.io/gitea/modules/util"
- "github.com/microcosm-cc/bluemonday/css"
"github.com/yuin/goldmark/ast"
east "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
@@ -26,10 +21,22 @@ import (
"github.com/yuin/goldmark/util"
)
-var byteMailto = []byte("mailto:")
-
// ASTTransformer is a default transformer of the goldmark tree.
-type ASTTransformer struct{}
+type ASTTransformer struct {
+ attentionTypes container.Set[string]
+}
+
+func NewASTTransformer() *ASTTransformer {
+ return &ASTTransformer{
+ attentionTypes: container.SetOf("note", "tip", "important", "warning", "caution"),
+ }
+}
+
+func (g *ASTTransformer) applyElementDir(n ast.Node) {
+ if markup.DefaultProcessorHelper.ElementDir != "" {
+ n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
+ }
+}
// Transform transforms the given AST tree.
func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
@@ -47,13 +54,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
tocMode = rc.TOC
}
- applyElementDir := func(n ast.Node) {
- if markup.DefaultProcessorHelper.ElementDir != "" {
- n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
- }
- }
-
- attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
@@ -61,129 +61,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
switch v := n.(type) {
case *ast.Heading:
- for _, attr := range v.Attributes() {
- if _, ok := attr.Value.([]byte); !ok {
- v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
- }
- }
- txt := n.Text(reader.Source())
- header := markup.Header{
- Text: util.BytesToReadOnlyString(txt),
- Level: v.Level,
- }
- if id, found := v.AttributeString("id"); found {
- header.ID = util.BytesToReadOnlyString(id.([]byte))
- }
- tocList = append(tocList, header)
- applyElementDir(v)
+ g.transformHeading(ctx, v, reader, &tocList)
case *ast.Paragraph:
- applyElementDir(v)
+ g.applyElementDir(v)
case *ast.Image:
- // Images need two things:
- //
- // 1. Their src needs to munged to be a real value
- // 2. If they're not wrapped with a link they need a link wrapper
-
- // Check if the destination is a real link
- if len(v.Destination) > 0 && !markup.IsLink(v.Destination) {
- v.Destination = []byte(giteautil.URLJoin(
- ctx.Links.ResolveMediaLink(ctx.IsWiki),
- strings.TrimLeft(string(v.Destination), "/"),
- ))
- }
-
- parent := n.Parent()
- // Create a link around image only if parent is not already a link
- if _, ok := parent.(*ast.Link); !ok && parent != nil {
- next := n.NextSibling()
-
- // Create a link wrapper
- wrap := ast.NewLink()
- wrap.Destination = v.Destination
- wrap.Title = v.Title
- wrap.SetAttributeString("target", []byte("_blank"))
-
- // Duplicate the current image node
- image := ast.NewImage(ast.NewLink())
- image.Destination = v.Destination
- image.Title = v.Title
- for _, attr := range v.Attributes() {
- image.SetAttribute(attr.Name, attr.Value)
- }
- for child := v.FirstChild(); child != nil; {
- next := child.NextSibling()
- image.AppendChild(image, child)
- child = next
- }
-
- // Append our duplicate image to the wrapper link
- wrap.AppendChild(wrap, image)
-
- // Wire in the next sibling
- wrap.SetNextSibling(next)
-
- // Replace the current node with the wrapper link
- parent.ReplaceChild(parent, n, wrap)
-
- // But most importantly ensure the next sibling is still on the old image too
- v.SetNextSibling(next)
- }
+ g.transformImage(ctx, v, reader)
case *ast.Link:
- // Links need their href to munged to be a real value
- link := v.Destination
- if len(link) > 0 && !markup.IsLink(link) &&
- link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
- // special case: this is not a link, a hash link or a mailto:, so it's a
- // relative URL
-
- var base string
- if ctx.IsWiki {
- base = ctx.Links.WikiLink()
- } else if ctx.Links.HasBranchInfo() {
- base = ctx.Links.SrcLink()
- } else {
- base = ctx.Links.Base
- }
-
- link = []byte(giteautil.URLJoin(base, string(link)))
- }
- if len(link) > 0 && link[0] == '#' {
- link = []byte("#user-content-" + string(link)[1:])
- }
- v.Destination = link
+ g.transformLink(ctx, v, reader)
case *ast.List:
- if v.HasChildren() {
- children := make([]ast.Node, 0, v.ChildCount())
- child := v.FirstChild()
- for child != nil {
- children = append(children, child)
- child = child.NextSibling()
- }
- v.RemoveChildren(v)
-
- for _, child := range children {
- listItem := child.(*ast.ListItem)
- if !child.HasChildren() || !child.FirstChild().HasChildren() {
- v.AppendChild(v, child)
- continue
- }
- taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
- if !ok {
- v.AppendChild(v, child)
- continue
- }
- newChild := NewTaskCheckBoxListItem(listItem)
- newChild.IsChecked = taskCheckBox.IsChecked
- newChild.SetAttributeString("class", []byte("task-list-item"))
- segments := newChild.FirstChild().Lines()
- if segments.Len() > 0 {
- segment := segments.At(0)
- newChild.SourcePosition = rc.metaLength + segment.Start
- }
- v.AppendChild(v, newChild)
- }
- }
- applyElementDir(v)
+ g.transformList(ctx, v, reader, rc)
case *ast.Text:
if v.SoftLineBreak() && !v.HardLineBreak() {
if ctx.Metas["mode"] != "document" {
@@ -193,22 +79,9 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
}
}
case *ast.CodeSpan:
- colorContent := n.Text(reader.Source())
- if css.ColorHandler(strings.ToLower(string(colorContent))) {
- v.AppendChild(v, NewColorPreview(colorContent))
- }
- case *ast.Emphasis:
- // check if inside blockquote for attention, expected hierarchy is
- // Emphasis < Paragraph < Blockquote
- blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote)
- if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) {
- fullText := string(n.Text(reader.Source()))
- if fullText == AttentionNote || fullText == AttentionWarning {
- v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText)))
- v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText))
- attentionMarkedBlockquotes.Add(blockquote)
- }
- }
+ g.transformCodeSpan(ctx, v, reader)
+ case *ast.Blockquote:
+ return g.transformBlockquote(v, reader)
}
return ast.WalkContinue, nil
})
@@ -230,55 +103,12 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
}
}
-type prefixedIDs struct {
- values container.Set[string]
-}
-
-// Generate generates a new element id.
-func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
- dft := []byte("id")
- if kind == ast.KindHeading {
- dft = []byte("heading")
- }
- return p.GenerateWithDefault(value, dft)
-}
-
-// Generate generates a new element id.
-func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
- result := common.CleanValue(value)
- if len(result) == 0 {
- result = dft
- }
- if !bytes.HasPrefix(result, []byte("user-content-")) {
- result = append([]byte("user-content-"), result...)
- }
- if p.values.Add(util.BytesToReadOnlyString(result)) {
- return result
- }
- for i := 1; ; i++ {
- newResult := fmt.Sprintf("%s-%d", result, i)
- if p.values.Add(newResult) {
- return []byte(newResult)
- }
- }
-}
-
-// Put puts a given element id to the used ids table.
-func (p *prefixedIDs) Put(value []byte) {
- p.values.Add(util.BytesToReadOnlyString(value))
-}
-
-func newPrefixedIDs() *prefixedIDs {
- return &prefixedIDs{
- values: make(container.Set[string]),
- }
-}
-
// NewHTMLRenderer creates a HTMLRenderer to render
// in the gitea form.
func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &HTMLRenderer{
- Config: html.NewConfig(),
+ Config: html.NewConfig(),
+ reValidName: regexp.MustCompile("^[a-z ]+$"),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
@@ -290,6 +120,7 @@ func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
// renders gitea specific features.
type HTMLRenderer struct {
html.Config
+ reValidName *regexp.Regexp
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
@@ -304,60 +135,6 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
}
-// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
-// See #21474 for reference
-func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
- if entering {
- if n.Attributes() != nil {
- _, _ = w.WriteString("')
- } else {
- _, _ = w.WriteString("")
- }
- for c := n.FirstChild(); c != nil; c = c.NextSibling() {
- switch v := c.(type) {
- case *ast.Text:
- segment := v.Segment
- value := segment.Value(source)
- if bytes.HasSuffix(value, []byte("\n")) {
- r.Writer.RawWrite(w, value[:len(value)-1])
- r.Writer.RawWrite(w, []byte(" "))
- } else {
- r.Writer.RawWrite(w, value)
- }
- case *ColorPreview:
- _, _ = w.WriteString(fmt.Sprintf(``, string(v.Color)))
- }
- }
- return ast.WalkSkipChildren, nil
- }
- _, _ = w.WriteString("
")
- return ast.WalkContinue, nil
-}
-
-// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
-func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- if entering {
- _, _ = w.WriteString(` \n")
- }
- return ast.WalkContinue, nil
-}
-
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Document)
@@ -417,8 +194,6 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, nil
}
-var validNameRE = regexp.MustCompile("^[a-z ]+$")
-
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
@@ -433,7 +208,7 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
return ast.WalkContinue, nil
}
- if !validNameRE.MatchString(name) {
+ if !r.reValidName.MatchString(name) {
// skip this
return ast.WalkContinue, nil
}
@@ -446,38 +221,3 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
return ast.WalkContinue, nil
}
-
-func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- n := node.(*TaskCheckBoxListItem)
- if entering {
- if n.Attributes() != nil {
- _, _ = w.WriteString("')
- } else {
- _, _ = w.WriteString(" ")
- }
- fmt.Fprintf(w, ``)
- } else {
- _ = w.WriteByte('>')
- }
- fc := n.FirstChild()
- if fc != nil {
- if _, ok := fc.(*ast.TextBlock); !ok {
- _ = w.WriteByte('\n')
- }
- }
- } else {
- _, _ = w.WriteString(" \n")
- }
- return ast.WalkContinue, nil
-}
-
-func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
- return ast.WalkContinue, nil
-}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 771162b9a3..db4e5706f6 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -6,6 +6,7 @@ package markdown
import (
"fmt"
+ "html/template"
"io"
"strings"
"sync"
@@ -103,7 +104,8 @@ func SpecializedMarkdown() goldmark.Markdown {
}
// include language-x class as part of commonmark spec
- _, err = w.WriteString(``)
+ // the "display" class is used by "js/markup/math.js" to render the code element as a block
+ _, err = w.WriteString(``)
if err != nil {
return
}
@@ -124,7 +126,7 @@ func SpecializedMarkdown() goldmark.Markdown {
parser.WithAttribute(),
parser.WithAutoHeadingID(),
parser.WithASTTransformers(
- util.Prioritized(&ASTTransformer{}, 10000),
+ util.Prioritized(NewASTTransformer(), 10000),
),
),
goldmark.WithRendererOptions(
@@ -262,12 +264,12 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
}
// RenderString renders Markdown string to HTML with all specific handling stuff and return string
-func RenderString(ctx *markup.RenderContext, content string) (string, error) {
+func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
var buf strings.Builder
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
return "", err
}
- return buf.String(), nil
+ return template.HTML(buf.String()), nil
}
// RenderRaw renders Markdown to HTML without handling special links.
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index bdf4011fa2..d9b67e43af 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -5,6 +5,7 @@ package markdown_test
import (
"context"
+ "html/template"
"os"
"strings"
"testing"
@@ -15,18 +16,20 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
const (
- AppURL = "http://localhost:3000/"
- Repo = "gogits/gogs"
- AppSubURL = AppURL + Repo + "/"
+ AppURL = "http://localhost:3000/"
+ FullURL = AppURL + "gogits/gogs/"
)
-// these values should match the Repo const above
+// these values should match the const above
var localMetas = map[string]string{
"user": "gogits",
"repo": "gogs",
@@ -48,34 +51,33 @@ func TestMain(m *testing.M) {
func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL
- setting.AppSubURL = AppSubURL
test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
- Base: setting.AppSubURL,
+ Base: FullURL,
},
}, input)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
buffer, err = markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
- Base: setting.AppSubURL,
+ Base: FullURL,
},
IsWiki: true,
}, input)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
+ assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
}
googleRendered := ``
test(" ", googleRendered, googleRendered)
- lnk := util.URLJoin(AppSubURL, "WikiPage")
- lnkWiki := util.URLJoin(AppSubURL, "wiki", "WikiPage")
+ lnk := util.URLJoin(FullURL, "WikiPage")
+ lnkWiki := util.URLJoin(FullURL, "wiki", "WikiPage")
test("[WikiPage](WikiPage)",
``,
``)
@@ -83,23 +85,22 @@ func TestRender_StandardLinks(t *testing.T) {
func TestRender_Images(t *testing.T) {
setting.AppURL = AppURL
- setting.AppSubURL = AppSubURL
test := func(input, expected string) {
buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
- Base: setting.AppSubURL,
+ Base: FullURL,
},
}, input)
assert.NoError(t, err)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
}
url := "../../.images/src/02/train.jpg"
title := "Train"
href := "https://gitea.io"
- result := util.URLJoin(AppSubURL, url)
+ result := util.URLJoin(FullURL, url)
// hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now
test(
@@ -132,11 +133,11 @@ func testAnswers(baseURLContent, baseURLImages string) []string {
Links, Language bindings, Engine bindings
Tips
-See commit 65f1bf27bc
+See commit 65f1bf27bc
Ideas and codes
-- Bezier widget (by @r-lyeh) ocornut/imgui#786
-- Bezier widget (by @r-lyeh) #786
+- Bezier widget (by @r-lyeh) ocornut/imgui#786
+- Bezier widget (by @r-lyeh) #786
- Node graph editors https://github.com/ocornut/imgui/issues/306
- Memory Editor
- Plot var helper
@@ -289,33 +290,32 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
func TestTotal_RenderWiki(t *testing.T) {
setting.AppURL = AppURL
- setting.AppSubURL = AppSubURL
- answers := testAnswers(util.URLJoin(AppSubURL, "wiki"), util.URLJoin(AppSubURL, "wiki", "raw"))
+ answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
for i := 0; i < len(sameCases); i++ {
line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
- Base: setting.AppSubURL,
+ Base: FullURL,
},
Metas: localMetas,
IsWiki: true,
}, sameCases[i])
assert.NoError(t, err)
- assert.Equal(t, answers[i], line)
+ assert.Equal(t, template.HTML(answers[i]), line)
}
testCases := []string{
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
- `Guardfile-DSL / Configuring-Guard
+ `Guardfile-DSL / Configuring-Guard
`,
// special syntax
`[[Name|Link]]`,
// rendered
- `
+ `
`,
}
@@ -323,32 +323,31 @@ func TestTotal_RenderWiki(t *testing.T) {
line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
- Base: setting.AppSubURL,
+ Base: FullURL,
},
IsWiki: true,
}, testCases[i])
assert.NoError(t, err)
- assert.Equal(t, testCases[i+1], line)
+ assert.Equal(t, template.HTML(testCases[i+1]), line)
}
}
func TestTotal_RenderString(t *testing.T) {
setting.AppURL = AppURL
- setting.AppSubURL = AppSubURL
- answers := testAnswers(util.URLJoin(AppSubURL, "src", "master"), util.URLJoin(AppSubURL, "media", "master"))
+ answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
for i := 0; i < len(sameCases); i++ {
line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
- Base: AppSubURL,
+ Base: FullURL,
BranchPath: "master",
},
Metas: localMetas,
}, sameCases[i])
assert.NoError(t, err)
- assert.Equal(t, answers[i], line)
+ assert.Equal(t, template.HTML(answers[i]), line)
}
testCases := []string{}
@@ -357,11 +356,11 @@ func TestTotal_RenderString(t *testing.T) {
line, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
Links: markup.Links{
- Base: AppSubURL,
+ Base: FullURL,
},
}, testCases[i])
assert.NoError(t, err)
- assert.Equal(t, testCases[i+1], line)
+ assert.Equal(t, template.HTML(testCases[i+1]), line)
}
}
@@ -428,7 +427,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
`
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
assert.NoError(t, err)
- assert.Equal(t, expected, res)
+ assert.Equal(t, template.HTML(expected), res)
}
func TestColorPreview(t *testing.T) {
@@ -437,6 +436,10 @@ func TestColorPreview(t *testing.T) {
testcase string
expected string
}{
+ { // do not render color names
+ "The CSS class `red` is there",
+ "The CSS class red
is there
\n",
+ },
{ // hex
"`#FF0000`",
`#FF0000
` + nl,
@@ -446,8 +449,8 @@ func TestColorPreview(t *testing.T) {
`rgb(16, 32, 64)
` + nl,
},
{ // short hex
- "This is the color white `#000`",
- `This is the color white #000
` + nl,
+ "This is the color white `#0a0`",
+ `This is the color white #0a0
` + nl,
},
{ // hsl
"HSL stands for hue, saturation, and lightness. An example: `hsl(0, 100%, 50%)`.",
@@ -462,7 +465,7 @@ func TestColorPreview(t *testing.T) {
for _, test := range positiveTests {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
- assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+ assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
}
@@ -508,9 +511,17 @@ func TestMathBlock(t *testing.T) {
`\(a\) \(b\)`,
`a
b
` + nl,
},
+ {
+ `$a$.`,
+ `a
.
` + nl,
+ },
+ {
+ `.$a$`,
+ `.$a$
` + nl,
+ },
{
`$a a$b b$`,
- `a a$b b
` + nl,
+ `$a a$b b$
` + nl,
},
{
`a a$b b`,
@@ -518,7 +529,15 @@ func TestMathBlock(t *testing.T) {
},
{
`a$b $a a$b b$`,
- `a$b a a$b b
` + nl,
+ `a$b $a a$b b$
` + nl,
+ },
+ {
+ "a$x$",
+ `a$x$
` + nl,
+ },
+ {
+ "$x$a",
+ `$x$a
` + nl,
},
{
"$$a$$",
@@ -529,7 +548,7 @@ func TestMathBlock(t *testing.T) {
for _, test := range testcases {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
- assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+ assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
}
}
@@ -567,12 +586,12 @@ foo: bar
for _, test := range testcases {
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
- assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+ assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
}
}
func TestRenderLinks(t *testing.T) {
- input := ` space @mention-user
+ input := ` space @mention-user${SPACE}${SPACE}
/just/a/path.bin
https://example.com/file.bin
[local link](file.bin)
@@ -593,8 +612,9 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
mail@domain.com
@mention-user test
#123
- space
+ space${SPACE}${SPACE}
`
+ input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming
cases := []struct {
Links markup.Links
IsWiki bool
@@ -633,9 +653,9 @@ space
Expected: `space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
-local link
+local link
remote link


@@ -691,9 +711,9 @@ space
Expected: `space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
-local link
+local link
remote link


@@ -749,9 +769,9 @@ space
Expected: `space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
-local link
+local link
remote link


@@ -809,9 +829,9 @@ space
Expected: `space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
-local link
+local link
remote link


@@ -869,9 +889,9 @@ space
Expected: `space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
-local link
+local link
remote link


@@ -931,9 +951,9 @@ space
Expected: `space @mention-user
/just/a/path.bin
https://example.com/file.bin
-local link
+local link
remote link
-local link
+local link
remote link


@@ -957,6 +977,39 @@ space
for i, c := range cases {
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
- assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i)
+ assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
}
}
+
+func TestAttention(t *testing.T) {
+ defer svg.MockIcon("octicon-info")()
+ defer svg.MockIcon("octicon-light-bulb")()
+ defer svg.MockIcon("octicon-report")()
+ defer svg.MockIcon("octicon-alert")()
+ defer svg.MockIcon("octicon-stop")()
+
+ renderAttention := func(attention, icon string) string {
+ tmpl := `{Attention}
`
+ tmpl = strings.ReplaceAll(tmpl, "{attention}", attention)
+ tmpl = strings.ReplaceAll(tmpl, "{icon}", icon)
+ tmpl = strings.ReplaceAll(tmpl, "{Attention}", cases.Title(language.English).String(attention))
+ return tmpl
+ }
+
+ test := func(input, expected string) {
+ result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, input)
+ assert.NoError(t, err)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
+ }
+
+ test(`
+> [!NOTE]
+> text
+`, renderAttention("note", "octicon-info")+"\ntext
\n
")
+
+ test(`> [!note]`, renderAttention("note", "octicon-info")+"\n")
+ test(`> [!tip]`, renderAttention("tip", "octicon-light-bulb")+"\n")
+ test(`> [!important]`, renderAttention("important", "octicon-report")+"\n")
+ test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n")
+ test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\n")
+}
diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go
index 0ac25c2b2a..862234e69b 100644
--- a/modules/markup/markdown/math/inline_parser.go
+++ b/modules/markup/markdown/math/inline_parser.go
@@ -41,9 +41,12 @@ func (parser *inlineParser) Trigger() []byte {
return parser.start[0:1]
}
+func isPunctuation(b byte) bool {
+ return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
+}
+
func isAlphanumeric(b byte) bool {
- // Github only cares about 0-9A-Za-z
- return (b >= '0' && b <= '9') || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
+ return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
}
// Parse parses the current line and returns a result of parsing.
@@ -56,7 +59,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
}
precedingCharacter := block.PrecendingCharacter()
- if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) {
+ if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
// need to exclude things like `a$` from being considered a start
return nil
}
@@ -75,14 +78,19 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
ender += pos
// Now we want to check the character at the end of our parser section
- // that is ender + len(parser.end)
+ // that is ender + len(parser.end) and check if char before ender is '\'
pos = ender + len(parser.end)
if len(line) <= pos {
break
}
- if !isAlphanumeric(line[pos]) {
+ suceedingCharacter := line[pos]
+ if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') {
+ return nil
+ }
+ if line[ender-1] != '\\' {
break
}
+
// move the pointer onwards
ender += len(parser.end)
}
diff --git a/modules/markup/markdown/prefixed_id.go b/modules/markup/markdown/prefixed_id.go
new file mode 100644
index 0000000000..9c60949202
--- /dev/null
+++ b/modules/markup/markdown/prefixed_id.go
@@ -0,0 +1,59 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "bytes"
+ "fmt"
+
+ "code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/markup/common"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/util"
+)
+
+type prefixedIDs struct {
+ values container.Set[string]
+}
+
+// Generate generates a new element id.
+func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
+ dft := []byte("id")
+ if kind == ast.KindHeading {
+ dft = []byte("heading")
+ }
+ return p.GenerateWithDefault(value, dft)
+}
+
+// GenerateWithDefault generates a new element id.
+func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
+ result := common.CleanValue(value)
+ if len(result) == 0 {
+ result = dft
+ }
+ if !bytes.HasPrefix(result, []byte("user-content-")) {
+ result = append([]byte("user-content-"), result...)
+ }
+ if p.values.Add(util.BytesToReadOnlyString(result)) {
+ return result
+ }
+ for i := 1; ; i++ {
+ newResult := fmt.Sprintf("%s-%d", result, i)
+ if p.values.Add(newResult) {
+ return []byte(newResult)
+ }
+ }
+}
+
+// Put puts a given element id to the used ids table.
+func (p *prefixedIDs) Put(value []byte) {
+ p.values.Add(util.BytesToReadOnlyString(value))
+}
+
+func newPrefixedIDs() *prefixedIDs {
+ return &prefixedIDs{
+ values: make(container.Set[string]),
+ }
+}
diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go
index 9602040931..38f744a25f 100644
--- a/modules/markup/markdown/toc.go
+++ b/modules/markup/markdown/toc.go
@@ -21,7 +21,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str
details.SetAttributeString(k, []byte(v))
}
- summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc"))))
+ summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc"))))
details.AppendChild(details, summary)
ul := ast.NewList('-')
details.AppendChild(details, ul)
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
new file mode 100644
index 0000000000..933f0e5c59
--- /dev/null
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -0,0 +1,98 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/svg"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
+func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ n := node.(*Attention)
+ var octiconName string
+ switch n.AttentionType {
+ case "tip":
+ octiconName = "light-bulb"
+ case "important":
+ octiconName = "report"
+ case "warning":
+ octiconName = "alert"
+ case "caution":
+ octiconName = "stop"
+ default: // including "note"
+ octiconName = "info"
+ }
+ _, _ = w.WriteString(string(svg.RenderHTML("octicon-"+octiconName, 16, "attention-icon attention-"+n.AttentionType)))
+ }
+ return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
+ // We only want attention blockquotes when the AST looks like:
+ // > Text("[") Text("!TYPE") Text("]")
+
+ // grab these nodes and make sure we adhere to the attention blockquote structure
+ firstParagraph := v.FirstChild()
+ g.applyElementDir(firstParagraph)
+ if firstParagraph.ChildCount() < 3 {
+ return ast.WalkContinue, nil
+ }
+ node1, ok := firstParagraph.FirstChild().(*ast.Text)
+ if !ok {
+ return ast.WalkContinue, nil
+ }
+ node2, ok := node1.NextSibling().(*ast.Text)
+ if !ok {
+ return ast.WalkContinue, nil
+ }
+ node3, ok := node2.NextSibling().(*ast.Text)
+ if !ok {
+ return ast.WalkContinue, nil
+ }
+ val1 := string(node1.Segment.Value(reader.Source()))
+ val2 := string(node2.Segment.Value(reader.Source()))
+ val3 := string(node3.Segment.Value(reader.Source()))
+ if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") {
+ return ast.WalkContinue, nil
+ }
+
+ // grab attention type from markdown source
+ attentionType := strings.ToLower(val2[1:])
+ if !g.attentionTypes.Contains(attentionType) {
+ return ast.WalkContinue, nil
+ }
+
+ // color the blockquote
+ v.SetAttributeString("class", []byte("attention-header attention-"+attentionType))
+
+ // create an emphasis to make it bold
+ attentionParagraph := ast.NewParagraph()
+ g.applyElementDir(attentionParagraph)
+ emphasis := ast.NewEmphasis(2)
+ emphasis.SetAttributeString("class", []byte("attention-"+attentionType))
+
+ attentionAstString := ast.NewString([]byte(cases.Title(language.English).String(attentionType)))
+
+ // replace the ![TYPE] with a dedicated paragraph of icon+Type
+ emphasis.AppendChild(emphasis, attentionAstString)
+ attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
+ attentionParagraph.AppendChild(attentionParagraph, emphasis)
+ firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
+ firstParagraph.RemoveChild(firstParagraph, node1)
+ firstParagraph.RemoveChild(firstParagraph, node2)
+ firstParagraph.RemoveChild(firstParagraph, node3)
+ if firstParagraph.ChildCount() == 0 {
+ firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
+ }
+ return ast.WalkContinue, nil
+}
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
new file mode 100644
index 0000000000..5b07d72999
--- /dev/null
+++ b/modules/markup/markdown/transform_codespan.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/microcosm-cc/bluemonday/css"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
+// See #21474 for reference
+func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString("")
+ }
+ for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+ switch v := c.(type) {
+ case *ast.Text:
+ segment := v.Segment
+ value := segment.Value(source)
+ if bytes.HasSuffix(value, []byte("\n")) {
+ r.Writer.RawWrite(w, value[:len(value)-1])
+ r.Writer.RawWrite(w, []byte(" "))
+ } else {
+ r.Writer.RawWrite(w, value)
+ }
+ case *ColorPreview:
+ _, _ = w.WriteString(fmt.Sprintf(``, string(v.Color)))
+ }
+ }
+ return ast.WalkSkipChildren, nil
+ }
+ _, _ = w.WriteString("
")
+ return ast.WalkContinue, nil
+}
+
+// cssColorHandler checks if a string is a render-able CSS color value.
+// The code is from "github.com/microcosm-cc/bluemonday/css.ColorHandler", except that it doesn't handle color words like "red".
+func cssColorHandler(value string) bool {
+ value = strings.ToLower(value)
+ if css.HexRGB.MatchString(value) {
+ return true
+ }
+ if css.RGB.MatchString(value) {
+ return true
+ }
+ if css.RGBA.MatchString(value) {
+ return true
+ }
+ if css.HSL.MatchString(value) {
+ return true
+ }
+ return css.HSLA.MatchString(value)
+}
+
+func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
+ colorContent := v.Text(reader.Source())
+ if cssColorHandler(string(colorContent)) {
+ v.AppendChild(v, NewColorPreview(colorContent))
+ }
+}
diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go
new file mode 100644
index 0000000000..ce585a37de
--- /dev/null
+++ b/modules/markup/markdown/transform_heading.go
@@ -0,0 +1,32 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+func (g *ASTTransformer) transformHeading(ctx *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
+ for _, attr := range v.Attributes() {
+ if _, ok := attr.Value.([]byte); !ok {
+ v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
+ }
+ }
+ txt := v.Text(reader.Source())
+ header := markup.Header{
+ Text: util.BytesToReadOnlyString(txt),
+ Level: v.Level,
+ }
+ if id, found := v.AttributeString("id"); found {
+ header.ID = util.BytesToReadOnlyString(id.([]byte))
+ }
+ *tocList = append(*tocList, header)
+ g.applyElementDir(v)
+}
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go
new file mode 100644
index 0000000000..f290dc3721
--- /dev/null
+++ b/modules/markup/markdown/transform_image.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/markup"
+ giteautil "code.gitea.io/gitea/modules/util"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image, reader text.Reader) {
+ // Images need two things:
+ //
+ // 1. Their src needs to munged to be a real value
+ // 2. If they're not wrapped with a link they need a link wrapper
+
+ // Check if the destination is a real link
+ if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
+ v.Destination = []byte(giteautil.URLJoin(
+ ctx.Links.ResolveMediaLink(ctx.IsWiki),
+ strings.TrimLeft(string(v.Destination), "/"),
+ ))
+ }
+
+ parent := v.Parent()
+ // Create a link around image only if parent is not already a link
+ if _, ok := parent.(*ast.Link); !ok && parent != nil {
+ next := v.NextSibling()
+
+ // Create a link wrapper
+ wrap := ast.NewLink()
+ wrap.Destination = v.Destination
+ wrap.Title = v.Title
+ wrap.SetAttributeString("target", []byte("_blank"))
+
+ // Duplicate the current image node
+ image := ast.NewImage(ast.NewLink())
+ image.Destination = v.Destination
+ image.Title = v.Title
+ for _, attr := range v.Attributes() {
+ image.SetAttribute(attr.Name, attr.Value)
+ }
+ for child := v.FirstChild(); child != nil; {
+ next := child.NextSibling()
+ image.AppendChild(image, child)
+ child = next
+ }
+
+ // Append our duplicate image to the wrapper link
+ wrap.AppendChild(wrap, image)
+
+ // Wire in the next sibling
+ wrap.SetNextSibling(next)
+
+ // Replace the current node with the wrapper link
+ parent.ReplaceChild(parent, v, wrap)
+
+ // But most importantly ensure the next sibling is still on the old image too
+ v.SetNextSibling(next)
+ }
+}
diff --git a/modules/markup/markdown/transform_link.go b/modules/markup/markdown/transform_link.go
new file mode 100644
index 0000000000..7e305b74bc
--- /dev/null
+++ b/modules/markup/markdown/transform_link.go
@@ -0,0 +1,42 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "path/filepath"
+
+ "code.gitea.io/gitea/modules/markup"
+ giteautil "code.gitea.io/gitea/modules/util"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+)
+
+func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
+ // Links need their href to munged to be a real value
+ link := v.Destination
+ isAnchorFragment := len(link) > 0 && link[0] == '#'
+ if !isAnchorFragment && !markup.IsFullURLBytes(link) {
+ base := ctx.Links.Base
+ if ctx.IsWiki {
+ if filepath.Ext(string(link)) == "" {
+ // This link doesn't have a file extension - assume a regular wiki link
+ base = ctx.Links.WikiLink()
+ } else if markup.Type(string(link)) != "" {
+ // If it's a file type we can render, use a regular wiki link
+ base = ctx.Links.WikiLink()
+ } else {
+ // Otherwise, use a raw link instead
+ base = ctx.Links.WikiRawLink()
+ }
+ } else if ctx.Links.HasBranchInfo() {
+ base = ctx.Links.SrcLink()
+ }
+ link = []byte(giteautil.URLJoin(base, string(link)))
+ }
+ if isAnchorFragment {
+ link = []byte("#user-content-" + string(link)[1:])
+ }
+ v.Destination = link
+}
diff --git a/modules/markup/markdown/transform_list.go b/modules/markup/markdown/transform_list.go
new file mode 100644
index 0000000000..6563e2dd64
--- /dev/null
+++ b/modules/markup/markdown/transform_list.go
@@ -0,0 +1,86 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markdown
+
+import (
+ "fmt"
+
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/yuin/goldmark/ast"
+ east "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*TaskCheckBoxListItem)
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("- ')
+ } else {
+ _, _ = w.WriteString("
- ")
+ }
+ fmt.Fprintf(w, ``)
+ } else {
+ _ = w.WriteByte('>')
+ }
+ fc := n.FirstChild()
+ if fc != nil {
+ if _, ok := fc.(*ast.TextBlock); !ok {
+ _ = w.WriteByte('\n')
+ }
+ }
+ } else {
+ _, _ = w.WriteString("
\n")
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ return ast.WalkContinue, nil
+}
+
+func (g *ASTTransformer) transformList(ctx *markup.RenderContext, v *ast.List, reader text.Reader, rc *RenderConfig) {
+ if v.HasChildren() {
+ children := make([]ast.Node, 0, v.ChildCount())
+ child := v.FirstChild()
+ for child != nil {
+ children = append(children, child)
+ child = child.NextSibling()
+ }
+ v.RemoveChildren(v)
+
+ for _, child := range children {
+ listItem := child.(*ast.ListItem)
+ if !child.HasChildren() || !child.FirstChild().HasChildren() {
+ v.AppendChild(v, child)
+ continue
+ }
+ taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
+ if !ok {
+ v.AppendChild(v, child)
+ continue
+ }
+ newChild := NewTaskCheckBoxListItem(listItem)
+ newChild.IsChecked = taskCheckBox.IsChecked
+ newChild.SetAttributeString("class", []byte("task-list-item"))
+ segments := newChild.FirstChild().Lines()
+ if segments.Len() > 0 {
+ segment := segments.At(0)
+ newChild.SourcePosition = rc.metaLength + segment.Start
+ }
+ v.AppendChild(v, newChild)
+ }
+ }
+ g.applyElementDir(v)
+}
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index ac1cedff6d..25f8d15ef4 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -136,17 +136,24 @@ type Writer struct {
func (r *Writer) resolveLink(kind, link string) string {
link = strings.TrimPrefix(link, "file:")
if !strings.HasPrefix(link, "#") && // not a URL fragment
- !markup.IsLinkStr(link) && // not an absolute URL
- !strings.HasPrefix(link, "mailto:") {
+ !markup.IsFullURLString(link) {
if kind == "regular" {
// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]"
// so we need to try to guess the link kind again here
kind = org.RegularLink{URL: link}.Kind()
}
+
base := r.Ctx.Links.Base
+ if r.Ctx.IsWiki {
+ base = r.Ctx.Links.WikiLink()
+ } else if r.Ctx.Links.HasBranchInfo() {
+ base = r.Ctx.Links.SrcLink()
+ }
+
if kind == "image" || kind == "video" {
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
}
+
link = util.URLJoin(base, link)
}
return link
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index 95f53c9cc9..75b60ed81f 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -19,6 +19,30 @@ const AppURL = "http://localhost:3000/"
func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL
+ test := func(input, expected string, isWiki bool) {
+ buffer, err := RenderString(&markup.RenderContext{
+ Ctx: git.DefaultContext,
+ Links: markup.Links{
+ Base: "/relative-path",
+ BranchPath: "branch/main",
+ },
+ IsWiki: isWiki,
+ }, input)
+ assert.NoError(t, err)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+ }
+
+ test("[[https://google.com/]]",
+ ``, false)
+ test("[[WikiPage][The WikiPage Desc]]",
+ ``, true)
+ test("[[ImageLink.svg][The Image Desc]]",
+ ``, false)
+}
+
+func TestRender_InternalLinks(t *testing.T) {
+ setting.AppURL = AppURL
+
test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
@@ -31,12 +55,14 @@ func TestRender_StandardLinks(t *testing.T) {
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
- test("[[https://google.com/]]",
- ``)
- test("[[WikiPage][The WikiPage Desc]]",
- ``)
- test("[[ImageLink.svg][The Image Desc]]",
- ``)
+ test("[[file:test.org][Test]]",
+ ``)
+ test("[[./test.org][Test]]",
+ ``)
+ test("[[test.org][Test]]",
+ ``)
+ test("[[path/to/test.org][Test]]",
+ ``)
}
func TestRender_Media(t *testing.T) {
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 5a7adcc553..005fcc278b 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
+ "html/template"
"io"
"net/url"
"path/filepath"
@@ -33,6 +34,8 @@ type ProcessorHelper struct {
IsUsernameMentionable func(ctx context.Context, username string) bool
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
+
+ RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
}
var DefaultProcessorHelper ProcessorHelper
@@ -82,9 +85,17 @@ type RenderContext struct {
}
type Links struct {
- Base string
- BranchPath string
- TreePath string
+ AbsolutePrefix bool
+ Base string
+ BranchPath string
+ TreePath string
+}
+
+func (l *Links) Prefix() string {
+ if l.AbsolutePrefix {
+ return setting.AppURL
+ }
+ return setting.AppSubURL
}
func (l *Links) HasBranchInfo() bool {
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 992e85b989..570a1da248 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -60,13 +60,28 @@ func createDefaultPolicy() *bluemonday.Policy {
// For JS code copy and Mermaid loading state
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
+ // For code preview
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
+ policy.AllowAttrs("data-line-number").OnElements("span")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
+
+ // For code preview (unicode escape)
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
+ policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
+
// For color preview
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
// For attention
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+$`)).OnElements("span", "strong")
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^svg octicon-\w+$`)).OnElements("svg")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
policy.AllowAttrs("fill-rule", "d").OnElements("path")
@@ -104,18 +119,12 @@ func createDefaultPolicy() *bluemonday.Policy {
// Allow icons
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
- // Allow unlabelled labels
- policy.AllowNoAttrs().OnElements("label")
-
// Allow classes for emojis
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
// Allow icons, emojis, chroma syntax and keyword markup on span
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
- // Allow 'style' attribute on text elements.
- policy.AllowAttrs("style").OnElements("span", "p")
-
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
policy.AllowStyles("color", "background-color").OnElements("span", "p")
@@ -143,7 +152,7 @@ func createDefaultPolicy() *bluemonday.Policy {
generalSafeElements := []string{
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
- "div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote",
+ "div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
"details", "caption", "figure", "figcaption",
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
diff --git a/modules/migration/messenger.go b/modules/migration/messenger.go
index 924aac9769..6f9cad3f10 100644
--- a/modules/migration/messenger.go
+++ b/modules/migration/messenger.go
@@ -3,7 +3,7 @@
package migration
-// Messenger is a formatting function similar to i18n.Tr
+// Messenger is a formatting function similar to i18n.TrString
type Messenger func(key string, args ...any)
// NilMessenger represents an empty formatting function
diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go
index 7ec345b6ba..4f55608004 100644
--- a/modules/optional/option_test.go
+++ b/modules/optional/option_test.go
@@ -1,48 +1,59 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
-package optional
+package optional_test
import (
"testing"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
"github.com/stretchr/testify/assert"
)
func TestOption(t *testing.T) {
- var uninitialized Option[int]
+ var uninitialized optional.Option[int]
assert.False(t, uninitialized.Has())
assert.Equal(t, int(0), uninitialized.Value())
assert.Equal(t, int(1), uninitialized.ValueOrDefault(1))
- none := None[int]()
+ none := optional.None[int]()
assert.False(t, none.Has())
assert.Equal(t, int(0), none.Value())
assert.Equal(t, int(1), none.ValueOrDefault(1))
- some := Some[int](1)
+ some := optional.Some[int](1)
assert.True(t, some.Has())
assert.Equal(t, int(1), some.Value())
assert.Equal(t, int(1), some.ValueOrDefault(2))
- var ptr *int
- assert.False(t, FromPtr(ptr).Has())
+ noneBool := optional.None[bool]()
+ assert.False(t, noneBool.Has())
+ assert.False(t, noneBool.Value())
+ assert.True(t, noneBool.ValueOrDefault(true))
- opt1 := FromPtr(util.ToPointer(1))
+ someBool := optional.Some(true)
+ assert.True(t, someBool.Has())
+ assert.True(t, someBool.Value())
+ assert.True(t, someBool.ValueOrDefault(false))
+
+ var ptr *int
+ assert.False(t, optional.FromPtr(ptr).Has())
+
+ int1 := 1
+ opt1 := optional.FromPtr(&int1)
assert.True(t, opt1.Has())
assert.Equal(t, int(1), opt1.Value())
- assert.False(t, FromNonDefault("").Has())
+ assert.False(t, optional.FromNonDefault("").Has())
- opt2 := FromNonDefault("test")
+ opt2 := optional.FromNonDefault("test")
assert.True(t, opt2.Has())
assert.Equal(t, "test", opt2.Value())
- assert.False(t, FromNonDefault(0).Has())
+ assert.False(t, optional.FromNonDefault(0).Has())
- opt3 := FromNonDefault(1)
+ opt3 := optional.FromNonDefault(1)
assert.True(t, opt3.Has())
assert.Equal(t, int(1), opt3.Value())
}
diff --git a/modules/optional/serialization.go b/modules/optional/serialization.go
new file mode 100644
index 0000000000..6688e78cd1
--- /dev/null
+++ b/modules/optional/serialization.go
@@ -0,0 +1,46 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional
+
+import (
+ "code.gitea.io/gitea/modules/json"
+
+ "gopkg.in/yaml.v3"
+)
+
+func (o *Option[T]) UnmarshalJSON(data []byte) error {
+ var v *T
+ if err := json.Unmarshal(data, &v); err != nil {
+ return err
+ }
+ *o = FromPtr(v)
+ return nil
+}
+
+func (o Option[T]) MarshalJSON() ([]byte, error) {
+ if !o.Has() {
+ return []byte("null"), nil
+ }
+
+ return json.Marshal(o.Value())
+}
+
+func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error {
+ var v *T
+ if err := value.Decode(&v); err != nil {
+ return err
+ }
+ *o = FromPtr(v)
+ return nil
+}
+
+func (o Option[T]) MarshalYAML() (interface{}, error) {
+ if !o.Has() {
+ return nil, nil
+ }
+
+ value := new(yaml.Node)
+ err := value.Encode(o.Value())
+ return value, err
+}
diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go
new file mode 100644
index 0000000000..09a4bddea0
--- /dev/null
+++ b/modules/optional/serialization_test.go
@@ -0,0 +1,190 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package optional_test
+
+import (
+ std_json "encoding/json" //nolint:depguard
+ "testing"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/optional"
+
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/yaml.v3"
+)
+
+type testSerializationStruct struct {
+ NormalString string `json:"normal_string" yaml:"normal_string"`
+ NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
+ OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
+ OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
+ OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"`
+ OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"`
+}
+
+func TestOptionalToJson(t *testing.T) {
+ tests := []struct {
+ name string
+ obj *testSerializationStruct
+ want string
+ }{
+ {
+ name: "empty",
+ obj: new(testSerializationStruct),
+ want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`,
+ },
+ {
+ name: "some",
+ obj: &testSerializationStruct{
+ NormalString: "a string",
+ NormalBool: true,
+ OptBool: optional.Some(false),
+ OptString: optional.Some(""),
+ OptTwoBool: optional.None[bool](),
+ OptTwoString: optional.None[string](),
+ },
+ want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ b, err := json.Marshal(tc.obj)
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.want, string(b), "gitea json module returned unexpected")
+
+ b, err = std_json.Marshal(tc.obj)
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.want, string(b), "std json module returned unexpected")
+ })
+ }
+}
+
+func TestOptionalFromJson(t *testing.T) {
+ tests := []struct {
+ name string
+ data string
+ want testSerializationStruct
+ }{
+ {
+ name: "empty",
+ data: `{}`,
+ want: testSerializationStruct{
+ NormalString: "",
+ },
+ },
+ {
+ name: "some",
+ data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
+ want: testSerializationStruct{
+ NormalString: "a string",
+ NormalBool: true,
+ OptBool: optional.Some(false),
+ OptString: optional.Some(""),
+ },
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var obj1 testSerializationStruct
+ err := json.Unmarshal([]byte(tc.data), &obj1)
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.want, obj1, "gitea json module returned unexpected")
+
+ var obj2 testSerializationStruct
+ err = std_json.Unmarshal([]byte(tc.data), &obj2)
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.want, obj2, "std json module returned unexpected")
+ })
+ }
+}
+
+func TestOptionalToYaml(t *testing.T) {
+ tests := []struct {
+ name string
+ obj *testSerializationStruct
+ want string
+ }{
+ {
+ name: "empty",
+ obj: new(testSerializationStruct),
+ want: `normal_string: ""
+normal_bool: false
+optional_two_bool: null
+optional_two_string: null
+`,
+ },
+ {
+ name: "some",
+ obj: &testSerializationStruct{
+ NormalString: "a string",
+ NormalBool: true,
+ OptBool: optional.Some(false),
+ OptString: optional.Some(""),
+ },
+ want: `normal_string: a string
+normal_bool: true
+optional_bool: false
+optional_string: ""
+optional_two_bool: null
+optional_two_string: null
+`,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ b, err := yaml.Marshal(tc.obj)
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.want, string(b), "yaml module returned unexpected")
+ })
+ }
+}
+
+func TestOptionalFromYaml(t *testing.T) {
+ tests := []struct {
+ name string
+ data string
+ want testSerializationStruct
+ }{
+ {
+ name: "empty",
+ data: ``,
+ want: testSerializationStruct{},
+ },
+ {
+ name: "empty but init",
+ data: `normal_string: ""
+normal_bool: false
+optional_bool:
+optional_two_bool:
+optional_two_string:
+`,
+ want: testSerializationStruct{},
+ },
+ {
+ name: "some",
+ data: `
+normal_string: a string
+normal_bool: true
+optional_bool: false
+optional_string: ""
+optional_two_bool: null
+optional_twostring: null
+`,
+ want: testSerializationStruct{
+ NormalString: "a string",
+ NormalBool: true,
+ OptBool: optional.Some(false),
+ OptString: optional.Some(""),
+ },
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var obj testSerializationStruct
+ err := yaml.Unmarshal([]byte(tc.data), &obj)
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.want, obj, "yaml module returned unexpected")
+ })
+ }
+}
diff --git a/modules/packages/alpine/metadata.go b/modules/packages/alpine/metadata.go
index 582c42610d..c492811744 100644
--- a/modules/packages/alpine/metadata.go
+++ b/modules/packages/alpine/metadata.go
@@ -34,6 +34,8 @@ const (
RepositoryPackage = "_alpine"
RepositoryVersion = "_repository"
+
+ NoArch = "noarch"
)
// https://wiki.alpinelinux.org/wiki/Apk_spec
diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index 3c478b1c02..6769c514cc 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -58,6 +58,7 @@ type Package struct {
type Metadata struct {
Description string `json:"description,omitempty"`
ReleaseNotes string `json:"release_notes,omitempty"`
+ Readme string `json:"readme,omitempty"`
Authors string `json:"authors,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
@@ -71,6 +72,7 @@ type Dependency struct {
Version string `json:"version"`
}
+// https://learn.microsoft.com/en-us/nuget/reference/nuspec
type nuspecPackage struct {
Metadata struct {
ID string `xml:"id"`
@@ -80,6 +82,7 @@ type nuspecPackage struct {
ProjectURL string `xml:"projectUrl"`
Description string `xml:"description"`
ReleaseNotes string `xml:"releaseNotes"`
+ Readme string `xml:"readme"`
PackageTypes struct {
PackageType []struct {
Name string `xml:"name,attr"`
@@ -89,6 +92,11 @@ type nuspecPackage struct {
URL string `xml:"url,attr"`
} `xml:"repository"`
Dependencies struct {
+ Dependency []struct {
+ ID string `xml:"id,attr"`
+ Version string `xml:"version,attr"`
+ Exclude string `xml:"exclude,attr"`
+ } `xml:"dependency"`
Group []struct {
TargetFramework string `xml:"targetFramework,attr"`
Dependency []struct {
@@ -122,14 +130,14 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
}
defer f.Close()
- return ParseNuspecMetaData(f)
+ return ParseNuspecMetaData(archive, f)
}
}
return nil, ErrMissingNuspecFile
}
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
-func ParseNuspecMetaData(r io.Reader) (*Package, error) {
+func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
var p nuspecPackage
if err := xml.NewDecoder(r).Decode(&p); err != nil {
return nil, err
@@ -166,6 +174,28 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
Dependencies: make(map[string][]Dependency),
}
+ if p.Metadata.Readme != "" {
+ f, err := archive.Open(p.Metadata.Readme)
+ if err == nil {
+ buf, _ := io.ReadAll(f)
+ m.Readme = string(buf)
+ _ = f.Close()
+ }
+ }
+
+ if len(p.Metadata.Dependencies.Dependency) > 0 {
+ deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
+ for _, dep := range p.Metadata.Dependencies.Dependency {
+ if dep.ID == "" || dep.Version == "" {
+ continue
+ }
+ deps = append(deps, Dependency{
+ ID: dep.ID,
+ Version: dep.Version,
+ })
+ }
+ m.Dependencies[""] = deps
+ }
for _, group := range p.Metadata.Dependencies.Group {
deps := make([]Dependency, 0, len(group.Dependency))
for _, dep := range group.Dependency {
diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go
index bba2bff4a5..f466492f8a 100644
--- a/modules/packages/nuget/metadata_test.go
+++ b/modules/packages/nuget/metadata_test.go
@@ -6,7 +6,6 @@ package nuget
import (
"archive/zip"
"bytes"
- "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -19,6 +18,7 @@ const (
projectURL = "https://gitea.io"
description = "Package Description"
releaseNotes = "Package Release Notes"
+ readme = "Readme"
repositoryURL = "https://gitea.io/gitea/gitea"
targetFramework = ".NETStandard2.1"
dependencyID = "System.Text.Json"
@@ -36,6 +36,7 @@ const nuspecContent = `
` + description + `
` + releaseNotes + `
+ README.md
@@ -60,17 +61,19 @@ const symbolsNuspecContent = `
`
func TestParsePackageMetaData(t *testing.T) {
- createArchive := func(name, content string) []byte {
+ createArchive := func(files map[string]string) []byte {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)
- w, _ := archive.Create(name)
- w.Write([]byte(content))
+ for name, content := range files {
+ w, _ := archive.Create(name)
+ w.Write([]byte(content))
+ }
archive.Close()
return buf.Bytes()
}
t.Run("MissingNuspecFile", func(t *testing.T) {
- data := createArchive("dummy.txt", "")
+ data := createArchive(map[string]string{"dummy.txt": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
@@ -78,7 +81,7 @@ func TestParsePackageMetaData(t *testing.T) {
})
t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
- data := createArchive("sub/package.nuspec", "")
+ data := createArchive(map[string]string{"sub/package.nuspec": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
@@ -86,7 +89,7 @@ func TestParsePackageMetaData(t *testing.T) {
})
t.Run("InvalidNuspecFile", func(t *testing.T) {
- data := createArchive("package.nuspec", "")
+ data := createArchive(map[string]string{"package.nuspec": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
@@ -94,10 +97,10 @@ func TestParsePackageMetaData(t *testing.T) {
})
t.Run("InvalidPackageId", func(t *testing.T) {
- data := createArchive("package.nuspec", `
+ data := createArchive(map[string]string{"package.nuspec": `
- `)
+ `})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
@@ -105,30 +108,34 @@ func TestParsePackageMetaData(t *testing.T) {
})
t.Run("InvalidPackageVersion", func(t *testing.T) {
- data := createArchive("package.nuspec", `
+ data := createArchive(map[string]string{"package.nuspec": `
- `+id+`
+ ` + id + `
- `)
+ `})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np)
assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
})
- t.Run("Valid", func(t *testing.T) {
- data := createArchive("package.nuspec", nuspecContent)
+ t.Run("MissingReadme", func(t *testing.T) {
+ data := createArchive(map[string]string{"package.nuspec": nuspecContent})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
+ assert.Empty(t, np.Metadata.Readme)
})
-}
-func TestParseNuspecMetaData(t *testing.T) {
t.Run("Dependency Package", func(t *testing.T) {
- np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent))
+ data := createArchive(map[string]string{
+ "package.nuspec": nuspecContent,
+ "README.md": readme,
+ })
+
+ np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
assert.Equal(t, DependencyPackage, np.PackageType)
@@ -139,6 +146,7 @@ func TestParseNuspecMetaData(t *testing.T) {
assert.Equal(t, projectURL, np.Metadata.ProjectURL)
assert.Equal(t, description, np.Metadata.Description)
assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
+ assert.Equal(t, readme, np.Metadata.Readme)
assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
assert.Len(t, np.Metadata.Dependencies, 1)
assert.Contains(t, np.Metadata.Dependencies, targetFramework)
@@ -148,13 +156,15 @@ func TestParseNuspecMetaData(t *testing.T) {
assert.Equal(t, dependencyVersion, deps[0].Version)
t.Run("NormalizedVersion", func(t *testing.T) {
- np, err := ParseNuspecMetaData(strings.NewReader(`
-
-
- test
- 1.04.5.2.5-rc.1+metadata
-
- `))
+ data := createArchive(map[string]string{"package.nuspec": `
+
+
+ test
+ 1.04.5.2.5-rc.1+metadata
+
+ `})
+
+ np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
assert.Equal(t, "1.4.5.2-rc.1", np.Version)
@@ -162,7 +172,9 @@ func TestParseNuspecMetaData(t *testing.T) {
})
t.Run("Symbols Package", func(t *testing.T) {
- np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent))
+ data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
+
+ np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, np)
assert.Equal(t, SymbolsPackage, np.PackageType)
diff --git a/modules/private/hook.go b/modules/private/hook.go
index cab8c81224..79c3d48229 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -11,6 +11,7 @@ import (
"time"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
)
@@ -32,13 +33,13 @@ const (
)
// Bool checks for a key in the map and parses as a boolean
-func (g GitPushOptions) Bool(key string, def bool) bool {
+func (g GitPushOptions) Bool(key string) optional.Option[bool] {
if val, ok := g[key]; ok {
if b, err := strconv.ParseBool(val); err == nil {
- return b
+ return optional.Some(b)
}
}
- return def
+ return optional.None[bool]()
}
// HookOptions represents the options for the Hook calls
@@ -87,13 +88,17 @@ type HookProcReceiveResult struct {
// HookProcReceiveRefResult represents an individual result from ProcReceive
type HookProcReceiveRefResult struct {
- OldOID string
- NewOID string
- Ref string
- OriginalRef git.RefName
- IsForcePush bool
- IsNotMatched bool
- Err string
+ OldOID string
+ NewOID string
+ Ref string
+ OriginalRef git.RefName
+ IsForcePush bool
+ IsNotMatched bool
+ Err string
+ IsCreatePR bool
+ URL string
+ ShouldShowMessage bool
+ HeadBranch string
}
// HookPreReceive check whether the provided commits are allowed
diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go
index 147a4f335e..e3801ef2b2 100644
--- a/modules/queue/workergroup.go
+++ b/modules/queue/workergroup.go
@@ -60,6 +60,9 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh
full = true
}
+ // TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum"
+ // The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later
+ // So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary.
q.workerNumMu.Lock()
noWorker := q.workerNum == 0
if full || noWorker {
@@ -143,7 +146,11 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
log.Debug("Queue %q starts new worker", q.GetName())
defer log.Debug("Queue %q stops idle worker", q.GetName())
+ atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging
+
t := time.NewTicker(workerIdleDuration)
+ defer t.Stop()
+
keepWorking := true
stopWorking := func() {
q.workerNumMu.Lock()
@@ -158,13 +165,18 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) {
case batch, ok := <-q.batchChan:
if !ok {
stopWorking()
- } else {
- q.doWorkerHandle(batch)
- t.Reset(workerIdleDuration)
+ continue
+ }
+ q.doWorkerHandle(batch)
+ // reset the idle ticker, and drain the tick after reset in case a tick is already triggered
+ t.Reset(workerIdleDuration)
+ select {
+ case <-t.C:
+ default:
}
case <-t.C:
q.workerNumMu.Lock()
- keepWorking = q.workerNum <= 1
+ keepWorking = q.workerNum <= 1 // keep the last worker running
if !keepWorking {
q.workerNum--
}
diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go
index b28fd88027..4160622d81 100644
--- a/modules/queue/workerqueue.go
+++ b/modules/queue/workerqueue.go
@@ -40,6 +40,8 @@ type WorkerPoolQueue[T any] struct {
workerMaxNum int
workerActiveNum int
workerNumMu sync.Mutex
+
+ workerStartedCounter int32
}
type flushType chan struct{}
diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go
index e60120162a..e09669c542 100644
--- a/modules/queue/workerqueue_test.go
+++ b/modules/queue/workerqueue_test.go
@@ -11,6 +11,7 @@ import (
"time"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
@@ -175,11 +176,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
}
func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
- oldWorkerIdleDuration := workerIdleDuration
- workerIdleDuration = 300 * time.Millisecond
- defer func() {
- workerIdleDuration = oldWorkerIdleDuration
- }()
+ defer test.MockVariableValue(&workerIdleDuration, 300*time.Millisecond)()
handler := func(items ...int) (unhandled []int) {
time.Sleep(100 * time.Millisecond)
@@ -250,3 +247,25 @@ func TestWorkerPoolQueueShutdown(t *testing.T) {
q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", qs, handler, false)
assert.EqualValues(t, 20, q.GetQueueItemNumber())
}
+
+func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) {
+ defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)()
+
+ handler := func(items ...int) (unhandled []int) {
+ time.Sleep(50 * time.Millisecond)
+ return nil
+ }
+
+ q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
+ stop := runWorkerPoolQueue(q)
+ for i := 0; i < 20; i++ {
+ assert.NoError(t, q.Push(i))
+ }
+
+ time.Sleep(500 * time.Millisecond)
+ assert.EqualValues(t, 2, q.GetWorkerNumber())
+ assert.EqualValues(t, 2, q.GetWorkerActiveNumber())
+ // when the queue never becomes empty, the existing workers should keep working
+ assert.EqualValues(t, 2, q.workerStartedCounter)
+ stop()
+}
diff --git a/modules/references/references.go b/modules/references/references.go
index 7758312564..761d6ee3d1 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -31,9 +31,9 @@ var (
// mentionPattern matches all mentions in the form of "@user" or "@org/team"
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
- issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\')([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
+ issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`)
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
- issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
+ issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`)
// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. org/repo#12345
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
diff --git a/modules/references/references_test.go b/modules/references/references_test.go
index ba7dda80cc..0c32933619 100644
--- a/modules/references/references_test.go
+++ b/modules/references/references_test.go
@@ -429,6 +429,8 @@ func TestRegExp_issueNumericPattern(t *testing.T) {
" #12",
"#12:",
"ref: #12: msg",
+ "\"#1234\"",
+ "'#1234'",
}
falseTestCases := []string{
"# 1234",
@@ -459,6 +461,8 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
"(ABC-123)",
"[ABC-123]",
"ABC-123:",
+ "\"ABC-123\"",
+ "'ABC-123'",
}
falseTestCases := []string{
"RC-08",
diff --git a/modules/repository/collaborator.go b/modules/repository/collaborator.go
index ebe14e3a4c..f71c58fbdf 100644
--- a/modules/repository/collaborator.go
+++ b/modules/repository/collaborator.go
@@ -16,6 +16,14 @@ import (
)
func AddCollaborator(ctx context.Context, repo *repo_model.Repository, u *user_model.User) error {
+ if err := repo.LoadOwner(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, u, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, repo.Owner, u.ID) {
+ return user_model.ErrBlockedUser
+ }
+
return db.WithTx(ctx, func(ctx context.Context) error {
has, err := db.Exist[repo_model.Collaboration](ctx, builder.Eq{
"repo_id": repo.ID,
diff --git a/modules/repository/create.go b/modules/repository/create.go
index 7c954a1412..4f18b9b3fa 100644
--- a/modules/repository/create.go
+++ b/modules/repository/create.go
@@ -87,7 +87,17 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: tp,
- Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle), AllowRebaseUpdate: true},
+ Config: &repo_model.PullRequestsConfig{
+ AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, AllowFastForwardOnly: true,
+ DefaultMergeStyle: repo_model.MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle),
+ AllowRebaseUpdate: true,
+ },
+ })
+ } else if tp == unit.TypeProjects {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
})
} else {
units = append(units, repo_model.RepoUnit{
@@ -143,7 +153,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
}
if setting.Service.AutoWatchNewRepos {
- if err = repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+ if err = repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
return fmt.Errorf("WatchRepo: %w", err)
}
}
diff --git a/modules/repository/init.go b/modules/repository/init.go
index 9c8e003bcb..57141d5ec7 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -6,15 +6,12 @@ package repository
import (
"context"
"fmt"
- "os"
"path/filepath"
"sort"
"strings"
- "time"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/label"
@@ -22,7 +19,6 @@ import (
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
)
type OptionFile struct {
@@ -125,70 +121,6 @@ func LoadRepoConfig() error {
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)
-
- sig := u.NewGitSig()
- // Because this may call hooks we should pass in the environment
- env := append(os.Environ(),
- "GIT_AUTHOR_NAME="+sig.Name,
- "GIT_AUTHOR_EMAIL="+sig.Email,
- "GIT_AUTHOR_DATE="+commitTimeStr,
- "GIT_COMMITTER_DATE="+commitTimeStr,
- )
- committerName := sig.Name
- committerEmail := sig.Email
-
- if stdout, _, err := git.NewCommand(ctx, "add", "--all").
- SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
- RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
- log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
- return fmt.Errorf("git add --all: %w", err)
- }
-
- cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
- AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
-
- sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
- if sign {
- cmd.AddOptionFormat("-S%s", keyID)
-
- if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
- // need to set the committer to the KeyID owner
- committerName = signer.Name
- committerEmail = signer.Email
- }
- } else {
- cmd.AddArguments("--no-gpg-sign")
- }
-
- env = append(env,
- "GIT_COMMITTER_NAME="+committerName,
- "GIT_COMMITTER_EMAIL="+committerEmail,
- )
-
- if stdout, _, err := cmd.
- SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
- RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
- log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
- return fmt.Errorf("git commit: %w", err)
- }
-
- if len(defaultBranch) == 0 {
- defaultBranch = setting.Repository.DefaultBranch
- }
-
- if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
- SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
- RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil {
- log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
- return fmt.Errorf("git push: %w", err)
- }
-
- return nil
-}
-
func CheckInitRepository(ctx context.Context, repo *repo_model.Repository, objectFormatName string) (err error) {
// Somehow the directory could exist.
isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index f9ea3ddc11..cb926084ba 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -5,16 +5,13 @@ package repository
import (
"context"
- "errors"
"fmt"
"io"
- "net/http"
"strings"
"time"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
@@ -22,10 +19,8 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
)
/*
@@ -47,244 +42,6 @@ func WikiRemoteURL(ctx context.Context, remote string) string {
return ""
}
-// MigrateRepositoryGitData starts migrating git related data after created migrating repository
-func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
- repo *repo_model.Repository, opts migration.MigrateOptions,
- httpTransport *http.Transport,
-) (*repo_model.Repository, error) {
- repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
-
- if u.IsOrganization() {
- t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
- if err != nil {
- return nil, err
- }
- repo.NumWatches = t.NumMembers
- } else {
- repo.NumWatches = 1
- }
-
- migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
-
- var err error
- if err = util.RemoveAll(repoPath); err != nil {
- return repo, fmt.Errorf("Failed to remove %s: %w", repoPath, err)
- }
-
- if err = git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
- Mirror: true,
- Quiet: true,
- Timeout: migrateTimeout,
- SkipTLSVerify: setting.Migrations.SkipTLSVerify,
- }); err != nil {
- if errors.Is(err, context.DeadlineExceeded) {
- return repo, fmt.Errorf("Clone timed out. Consider increasing [git.timeout] MIGRATE in app.ini. Underlying Error: %w", err)
- }
- return repo, fmt.Errorf("Clone: %w", err)
- }
-
- if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
- return repo, err
- }
-
- if opts.Wiki {
- wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
- wikiRemotePath := WikiRemoteURL(ctx, opts.CloneAddr)
- if len(wikiRemotePath) > 0 {
- if err := util.RemoveAll(wikiPath); err != nil {
- return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
- }
-
- if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
- Mirror: true,
- Quiet: true,
- Timeout: migrateTimeout,
- Branch: "master",
- SkipTLSVerify: setting.Migrations.SkipTLSVerify,
- }); err != nil {
- log.Warn("Clone wiki: %v", err)
- if err := util.RemoveAll(wikiPath); err != nil {
- return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
- }
- } else {
- if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
- return repo, err
- }
- }
- }
- }
-
- if repo.OwnerID == u.ID {
- repo.Owner = u
- }
-
- if err = CheckDaemonExportOK(ctx, repo); err != nil {
- return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
- }
-
- if stdout, _, err := git.NewCommand(ctx, "update-server-info").
- SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
- RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
- log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
- return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
- }
-
- gitRepo, err := git.OpenRepository(ctx, repoPath)
- if err != nil {
- return repo, fmt.Errorf("OpenRepository: %w", err)
- }
- defer gitRepo.Close()
-
- repo.IsEmpty, err = gitRepo.IsEmpty()
- if err != nil {
- return repo, fmt.Errorf("git.IsEmpty: %w", err)
- }
-
- if !repo.IsEmpty {
- if len(repo.DefaultBranch) == 0 {
- // Try to get HEAD branch and set it as default branch.
- headBranch, err := gitRepo.GetHEADBranch()
- if err != nil {
- return repo, fmt.Errorf("GetHEADBranch: %w", err)
- }
- if headBranch != nil {
- repo.DefaultBranch = headBranch.Name
- }
- }
-
- if _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
- return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err)
- }
-
- if !opts.Releases {
- // note: this will greatly improve release (tag) sync
- // for pull-mirrors with many tags
- repo.IsMirror = opts.Mirror
- if err = SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
- log.Error("Failed to synchronize tags to releases for repository: %v", err)
- }
- }
-
- if opts.LFS {
- endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
- lfsClient := lfs.NewClient(endpoint, httpTransport)
- if err = StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
- log.Error("Failed to store missing LFS objects for repository: %v", err)
- }
- }
- }
-
- ctx, committer, err := db.TxContext(ctx)
- if err != nil {
- return nil, err
- }
- defer committer.Close()
-
- if opts.Mirror {
- remoteAddress, err := util.SanitizeURL(opts.CloneAddr)
- if err != nil {
- return repo, err
- }
- mirrorModel := repo_model.Mirror{
- RepoID: repo.ID,
- Interval: setting.Mirror.DefaultInterval,
- EnablePrune: true,
- NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
- LFS: opts.LFS,
- RemoteAddress: remoteAddress,
- }
- if opts.LFS {
- mirrorModel.LFSEndpoint = opts.LFSEndpoint
- }
-
- if opts.MirrorInterval != "" {
- parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
- if err != nil {
- log.Error("Failed to set Interval: %v", err)
- return repo, err
- }
- if parsedInterval == 0 {
- mirrorModel.Interval = 0
- mirrorModel.NextUpdateUnix = 0
- } else if parsedInterval < setting.Mirror.MinInterval {
- err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
- log.Error("Interval: %s is too frequent", opts.MirrorInterval)
- return repo, err
- } else {
- mirrorModel.Interval = parsedInterval
- mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
- }
- }
-
- if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
- return repo, fmt.Errorf("InsertOne: %w", err)
- }
-
- repo.IsMirror = true
- if err = UpdateRepository(ctx, repo, false); err != nil {
- return nil, err
- }
-
- // this is necessary for sync local tags from remote
- configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName())
- if stdout, _, err := git.NewCommand(ctx, "config").
- AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`).
- RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
- log.Error("MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err)
- return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*): %w", err)
- }
- } else {
- if err = UpdateRepoSize(ctx, repo); err != nil {
- log.Error("Failed to update size for repository: %v", err)
- }
- if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
- return nil, err
- }
- }
-
- return repo, committer.Commit()
-}
-
-// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
-// This also removes possible user credentials.
-func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
- cmd := git.NewCommand(ctx, "remote", "rm", "origin")
- // if the origin does not exist
- _, stderr, err := cmd.RunStdString(&git.RunOpts{
- Dir: repoPath,
- })
- if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
- return err
- }
- return nil
-}
-
-// 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) {
- repoPath := repo.RepoPath()
- if err := gitrepo.CreateDelegateHooks(ctx, repo, false); err != nil {
- return repo, fmt.Errorf("createDelegateHooks: %w", err)
- }
- if repo.HasWiki() {
- if err := gitrepo.CreateDelegateHooks(ctx, repo, true); err != nil {
- return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
- }
- }
-
- _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
- if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
- return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
- }
-
- if repo.HasWiki() {
- if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil {
- return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
- }
- }
-
- return repo, UpdateRepository(ctx, repo, false)
-}
-
// SyncRepoTags synchronizes releases table with repository tags
func SyncRepoTags(ctx context.Context, repoID int64) error {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
@@ -352,7 +109,9 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
}
if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil {
- return fmt.Errorf("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %w", tagName, repo.ID, repo.OwnerName, repo.Name, err)
+ // sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11
+ // this is a tree object, not a tag object which created before git
+ log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err)
}
return nil
diff --git a/modules/secret/secret.go b/modules/secret/secret.go
index 9c2ecd181d..e70ae1839c 100644
--- a/modules/secret/secret.go
+++ b/modules/secret/secret.go
@@ -7,13 +7,12 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
+ "crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
-
- "github.com/minio/sha256-simd"
)
// AesEncrypt encrypts text and given key with AES.
diff --git a/modules/setting/admin.go b/modules/setting/admin.go
index 2d2dd26de9..8aebc76154 100644
--- a/modules/setting/admin.go
+++ b/modules/setting/admin.go
@@ -3,14 +3,28 @@
package setting
+import (
+ "code.gitea.io/gitea/modules/container"
+)
+
// Admin settings
var Admin struct {
- DisableRegularOrgCreation bool
- DefaultEmailNotification string
+ DisableRegularOrgCreation bool
+ DefaultEmailNotification string
+ UserDisabledFeatures container.Set[string]
+ ExternalUserDisableFeatures container.Set[string]
}
func loadAdminFrom(rootCfg ConfigProvider) {
- mustMapSetting(rootCfg, "admin", &Admin)
sec := rootCfg.Section("admin")
+ Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
+ Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
+ Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...)
}
+
+const (
+ UserFeatureDeletion = "deletion"
+ UserFeatureManageSSHKeys = "manage_ssh_keys"
+ UserFeatureManageGPGKeys = "manage_gpg_keys"
+)
diff --git a/modules/setting/attachment.go b/modules/setting/attachment.go
index 934d4d7f46..0fdabb5032 100644
--- a/modules/setting/attachment.go
+++ b/modules/setting/attachment.go
@@ -12,7 +12,7 @@ var Attachment = struct {
Enabled bool
}{
Storage: &Storage{},
- AllowedTypes: ".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
+ AllowedTypes: ".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
MaxSize: 2048,
MaxFiles: 5,
Enabled: true,
@@ -25,7 +25,7 @@ func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
return err
}
- Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
+ Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(2048)
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
diff --git a/modules/setting/config.go b/modules/setting/config.go
index db189f44ac..03558574c2 100644
--- a/modules/setting/config.go
+++ b/modules/setting/config.go
@@ -15,8 +15,45 @@ type PictureStruct struct {
EnableFederatedAvatar *config.Value[bool]
}
+type OpenWithEditorApp struct {
+ DisplayName string
+ OpenURL string
+}
+
+type OpenWithEditorAppsType []OpenWithEditorApp
+
+func (t OpenWithEditorAppsType) ToTextareaString() string {
+ ret := ""
+ for _, app := range t {
+ ret += app.DisplayName + " = " + app.OpenURL + "\n"
+ }
+ return ret
+}
+
+func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
+ return OpenWithEditorAppsType{
+ {
+ DisplayName: "VS Code",
+ OpenURL: "vscode://vscode.git/clone?url={url}",
+ },
+ {
+ DisplayName: "VSCodium",
+ OpenURL: "vscodium://vscode.git/clone?url={url}",
+ },
+ {
+ DisplayName: "Intellij IDEA",
+ OpenURL: "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
+ },
+ }
+}
+
+type RepositoryStruct struct {
+ OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
+}
+
type ConfigStruct struct {
- Picture *PictureStruct
+ Picture *PictureStruct
+ Repository *RepositoryStruct
}
var (
@@ -28,8 +65,11 @@ func initDefaultConfig() {
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
defaultConfig = &ConfigStruct{
Picture: &PictureStruct{
- DisableGravatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
- EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
+ DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
+ EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
+ },
+ Repository: &RepositoryStruct{
+ OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
},
}
}
@@ -42,6 +82,9 @@ func Config() *ConfigStruct {
type cfgSecKeyGetter struct{}
func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
+ if key == "" {
+ return "", false
+ }
cfgSec, err := CfgProvider.GetSection(sec)
if err != nil {
log.Error("Unable to get config section: %q", sec)
diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go
index 817fcdb786..f0ec120544 100644
--- a/modules/setting/config/value.go
+++ b/modules/setting/config/value.go
@@ -5,8 +5,11 @@ package config
import (
"context"
- "strconv"
"sync"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
)
type CfgSecKey struct {
@@ -23,14 +26,14 @@ type Value[T any] struct {
revision int
}
-func (value *Value[T]) parse(s string) (v T) {
- switch any(v).(type) {
- case bool:
- b, _ := strconv.ParseBool(s)
- return any(b).(T)
- default:
- panic("unsupported config type, please complete the code")
+func (value *Value[T]) parse(key, valStr string) (v T) {
+ v = value.def
+ if valStr != "" {
+ if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
+ log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
+ }
}
+ return v
}
func (value *Value[T]) Value(ctx context.Context) (v T) {
@@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
if valStr == nil {
v = value.def
} else {
- v = value.parse(*valStr)
+ v = value.parse(value.dynKey, *valStr)
}
value.mu.Lock()
@@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
return value.dynKey
}
-func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
- return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
+func (value *Value[T]) WithDefault(def T) *Value[T] {
+ value.def = def
+ return value
+}
+
+func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
+ value.cfgSecKey = cfgSecKey
+ return value
+}
+
+func ValueJSON[T any](dynKey string) *Value[T] {
+ return &Value[T]{dynKey: dynKey}
}
diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go
index 132f4acea1..03f27ba203 100644
--- a/modules/setting/config_provider.go
+++ b/modules/setting/config_provider.go
@@ -196,7 +196,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
// NewConfigProviderFromFile load configuration from file.
// NOTE: do not print any log except error.
-func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
+func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
cfg := ini.Empty(configProviderLoadOptions())
loadedFromEmpty := true
@@ -213,12 +213,6 @@ func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvi
}
}
- for _, s := range extraConfigs {
- if err := cfg.Append([]byte(s)); err != nil {
- return nil, fmt.Errorf("unable to append more config: %v", err)
- }
- }
-
cfg.NameMapper = ini.SnackCase
return &iniConfigProvider{
file: file,
@@ -321,21 +315,25 @@ func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
}
}
-// DeprecatedWarnings contains the warning message for various deprecations, including: setting option, file/folder, etc
-var DeprecatedWarnings []string
+// StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc
+var StartupProblems []string
+
+func logStartupProblem(skip int, level log.Level, format string, args ...any) {
+ msg := fmt.Sprintf(format, args...)
+ log.Log(skip+1, level, "%s", msg)
+ StartupProblems = append(StartupProblems, msg)
+}
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
if rootCfg.Section(oldSection).HasKey(oldKey) {
- msg := fmt.Sprintf("Deprecated config option `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
- log.Error("%v", msg)
- DeprecatedWarnings = append(DeprecatedWarnings, msg)
+ logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
}
}
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
if rootCfg.Section(oldSection).HasKey(oldKey) {
- log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
+ logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
}
}
diff --git a/modules/setting/database.go b/modules/setting/database.go
index e200b15b2e..1a4bf64805 100644
--- a/modules/setting/database.go
+++ b/modules/setting/database.go
@@ -25,26 +25,27 @@ var (
// Database holds the database settings
Database = struct {
- Type DatabaseType
- Host string
- Name string
- User string
- Passwd string
- Schema string
- SSLMode string
- Path string
- LogSQL bool
- MysqlCharset string
- CharsetCollation string
- Timeout int // seconds
- SQLiteJournalMode string
- DBConnectRetries int
- DBConnectBackoff time.Duration
- MaxIdleConns int
- MaxOpenConns int
- ConnMaxLifetime time.Duration
- IterateBufferSize int
- AutoMigration bool
+ Type DatabaseType
+ Host string
+ Name string
+ User string
+ Passwd string
+ Schema string
+ SSLMode string
+ Path string
+ LogSQL bool
+ MysqlCharset string
+ CharsetCollation string
+ Timeout int // seconds
+ SQLiteJournalMode string
+ DBConnectRetries int
+ DBConnectBackoff time.Duration
+ MaxIdleConns int
+ MaxOpenConns int
+ ConnMaxLifetime time.Duration
+ IterateBufferSize int
+ AutoMigration bool
+ SlowQueryThreshold time.Duration
}{
Timeout: 500,
IterateBufferSize: 50,
@@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
+ Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
}
// DBConnStr returns database connection string
diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index 16f3d80168..6877d70e3c 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -53,21 +53,24 @@ var Indexer = struct {
func loadIndexerFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("indexer")
Indexer.IssueType = sec.Key("ISSUE_INDEXER_TYPE").MustString("bleve")
- Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
- if !filepath.IsAbs(Indexer.IssuePath) {
- Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
- }
- Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
-
- if Indexer.IssueType == "meilisearch" {
- u, err := url.Parse(Indexer.IssueConnStr)
- if err != nil {
- log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
- u = &url.URL{}
+ if Indexer.IssueType == "bleve" {
+ Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
+ if !filepath.IsAbs(Indexer.IssuePath) {
+ Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
+ }
+ checkOverlappedPath("[indexer].ISSUE_INDEXER_PATH", Indexer.IssuePath)
+ } else {
+ Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
+ if Indexer.IssueType == "meilisearch" {
+ u, err := url.Parse(Indexer.IssueConnStr)
+ if err != nil {
+ log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
+ u = &url.URL{}
+ }
+ Indexer.IssueConnAuth, _ = u.User.Password()
+ u.User = nil
+ Indexer.IssueConnStr = u.String()
}
- Indexer.IssueConnAuth, _ = u.User.Password()
- u.User = nil
- Indexer.IssueConnStr = u.String()
}
Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName)
diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go
index a5ea537cef..2034ef782c 100644
--- a/modules/setting/lfs.go
+++ b/modules/setting/lfs.go
@@ -4,22 +4,19 @@
package setting
import (
- "encoding/base64"
"fmt"
"time"
"code.gitea.io/gitea/modules/generate"
- "code.gitea.io/gitea/modules/util"
)
// LFS represents the configuration for Git LFS
var LFS = struct {
- StartServer bool `ini:"LFS_START_SERVER"`
- JWTSecretBase64 string `ini:"LFS_JWT_SECRET"`
- JWTSecretBytes []byte `ini:"-"`
- HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
- MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
- LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
+ StartServer bool `ini:"LFS_START_SERVER"`
+ JWTSecretBytes []byte `ini:"-"`
+ HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
+ MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
+ LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
Storage *Storage
}{}
@@ -61,10 +58,10 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
return nil
}
- LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
- LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32)
+ jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
+ LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
if err != nil {
- LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64()
+ LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
}
@@ -74,8 +71,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
if err != nil {
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
}
- rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
- saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
+ rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
+ saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
if err := saveCfg.Save(); err != nil {
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
}
diff --git a/modules/setting/log.go b/modules/setting/log.go
index e404074b72..50c5779994 100644
--- a/modules/setting/log.go
+++ b/modules/setting/log.go
@@ -185,8 +185,13 @@ func InitLoggersForTest() {
initAllLoggers()
}
+var initLoggerDisabled bool
+
// initAllLoggers creates all the log services
func initAllLoggers() {
+ if initLoggerDisabled {
+ return
+ }
initManagedLoggers(log.GetManager(), CfgProvider)
golog.SetFlags(0)
@@ -194,6 +199,10 @@ func initAllLoggers() {
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
}
+func DisableLoggerInit() {
+ initLoggerDisabled = true
+}
+
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
loadLogGlobalFrom(cfg)
prepareLoggerConfig(cfg)
diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go
index 0d15e91ef0..830472db32 100644
--- a/modules/setting/oauth2.go
+++ b/modules/setting/oauth2.go
@@ -4,13 +4,12 @@
package setting
import (
- "encoding/base64"
"math"
"path/filepath"
+ "sync/atomic"
"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
)
// OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
@@ -98,7 +97,6 @@ var OAuth2 = struct {
RefreshTokenExpirationTime int64
InvalidateRefreshTokens bool
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
- JWTSecretBase64 string `ini:"JWT_SECRET"`
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
MaxTokenLength int
DefaultApplications []string
@@ -120,6 +118,10 @@ func loadOAuth2From(rootCfg ConfigProvider) {
return
}
+ if sec.HasKey("DEFAULT_APPLICATIONS") && sec.Key("DEFAULT_APPLICATIONS").String() == "" {
+ OAuth2.DefaultApplications = nil
+ }
+
// Handle the rename of ENABLE to ENABLED
deprecatedSetting(rootCfg, "oauth2", "ENABLE", "oauth2", "ENABLED", "v1.23.0")
if sec.HasKey("ENABLE") && !sec.HasKey("ENABLED") {
@@ -130,29 +132,50 @@ func loadOAuth2From(rootCfg ConfigProvider) {
return
}
- OAuth2.JWTSecretBase64 = loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
+ jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
}
if InstallLock {
- if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil {
- key, err := generate.NewJwtSecret()
+ jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
+ if err != nil {
+ jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
log.Fatal("error generating JWT secret: %v", err)
}
-
- OAuth2.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(key)
saveCfg, err := rootCfg.PrepareSaving()
if err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
}
- rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
- saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
+ rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
+ saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
if err := saveCfg.Save(); err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
}
}
+ generalSigningSecret.Store(&jwtSecretBytes)
}
}
+
+// generalSigningSecret is used as container for a []byte value
+// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
+var generalSigningSecret atomic.Pointer[[]byte]
+
+func GetGeneralTokenSigningSecret() []byte {
+ old := generalSigningSecret.Load()
+ if old == nil || len(*old) == 0 {
+ jwtSecret, _, err := generate.NewJwtSecretWithBase64()
+ if err != nil {
+ log.Fatal("Unable to generate general JWT secret: %s", err.Error())
+ }
+ if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
+ // FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
+ logStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
+ return jwtSecret
+ }
+ return *generalSigningSecret.Load()
+ }
+ return *old
+}
diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go
new file mode 100644
index 0000000000..4403f35892
--- /dev/null
+++ b/modules/setting/oauth2_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/modules/generate"
+ "code.gitea.io/gitea/modules/test"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetGeneralSigningSecret(t *testing.T) {
+ // when there is no general signing secret, it should be generated, and keep the same value
+ assert.Nil(t, generalSigningSecret.Load())
+ s1 := GetGeneralTokenSigningSecret()
+ assert.NotNil(t, s1)
+ s2 := GetGeneralTokenSigningSecret()
+ assert.Equal(t, s1, s2)
+
+ // the config value should always override any pre-generated value
+ cfg, _ := NewConfigProviderFromData(`
+[oauth2]
+JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+`)
+ defer test.MockVariableValue(&InstallLock, true)()
+ loadOAuth2From(cfg)
+ actual := GetGeneralTokenSigningSecret()
+ expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
+ assert.Len(t, actual, 32)
+ assert.EqualValues(t, expected, actual)
+}
+
+func TestOauth2DefaultApplications(t *testing.T) {
+ cfg, _ := NewConfigProviderFromData(``)
+ loadOAuth2From(cfg)
+ assert.Equal(t, []string{"git-credential-oauth", "git-credential-manager", "tea"}, OAuth2.DefaultApplications)
+
+ cfg, _ = NewConfigProviderFromData(`[oauth2]
+DEFAULT_APPLICATIONS = tea
+`)
+ loadOAuth2From(cfg)
+ assert.Equal(t, []string{"tea"}, OAuth2.DefaultApplications)
+
+ cfg, _ = NewConfigProviderFromData(`[oauth2]
+DEFAULT_APPLICATIONS =
+`)
+ loadOAuth2From(cfg)
+ assert.Nil(t, nil, OAuth2.DefaultApplications)
+}
diff --git a/modules/setting/other.go b/modules/setting/other.go
index 706cb1e3d9..4ba494765b 100644
--- a/modules/setting/other.go
+++ b/modules/setting/other.go
@@ -8,6 +8,7 @@ import "code.gitea.io/gitea/modules/log"
type OtherConfig struct {
ShowFooterVersion bool
ShowFooterTemplateLoadTime bool
+ ShowFooterPoweredBy bool
EnableFeed bool
EnableSitemap bool
}
@@ -15,6 +16,7 @@ type OtherConfig struct {
var Other = OtherConfig{
ShowFooterVersion: true,
ShowFooterTemplateLoadTime: true,
+ ShowFooterPoweredBy: true,
EnableSitemap: true,
EnableFeed: true,
}
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index a6f0ed8833..8656ebc7ec 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -285,6 +285,9 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
} else {
RepoRootPath = filepath.Clean(RepoRootPath)
}
+
+ checkOverlappedPath("[repository].ROOT", RepoRootPath)
+
defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
for _, charset := range Repository.DetectedCharsetsOrder {
defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset)))
diff --git a/modules/setting/security.go b/modules/setting/security.go
index 380360a696..3d7b1f9ce7 100644
--- a/modules/setting/security.go
+++ b/modules/setting/security.go
@@ -103,7 +103,7 @@ func generateSaveInternalToken(rootCfg ConfigProvider) {
func loadSecurityFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("security")
InstallLock = HasInstallLock(rootCfg)
- LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
+ LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31)
SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
if SecretKey == "" {
// FIXME: https://github.com/go-gitea/gitea/issues/16832
diff --git a/modules/setting/server.go b/modules/setting/server.go
index c09b91612a..7d6ece2727 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -7,7 +7,6 @@ import (
"encoding/base64"
"net"
"net/url"
- "path"
"path/filepath"
"strconv"
"strings"
@@ -321,17 +320,18 @@ func loadServerFrom(rootCfg ConfigProvider) {
}
StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath)
StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour)
- AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data"))
+ AppDataPath = sec.Key("APP_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data"))
if !filepath.IsAbs(AppDataPath) {
AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
}
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
- PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(path.Join(AppWorkPath, "data/tmp/pprof"))
+ PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
if !filepath.IsAbs(PprofDataPath) {
PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
}
+ checkOverlappedPath("[server].PPROF_DATA_PATH", PprofDataPath)
landingPage := sec.Key("LANDING_PAGE").MustString("home")
switch landingPage {
diff --git a/modules/setting/session.go b/modules/setting/session.go
index 664c66f869..afe63bfdb7 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -5,7 +5,6 @@ package setting
import (
"net/http"
- "path"
"path/filepath"
"strings"
@@ -21,7 +20,7 @@ var SessionConfig = struct {
ProviderConfig string
// Cookie name to save session ID. Default is "MacaronSession".
CookieName string
- // Cookie path to store. Default is "/". HINT: there was a bug, the old value doesn't have trailing slash, and could be empty "".
+ // Cookie path to store. Default is "/".
CookiePath string
// GC interval time in seconds. Default is 3600.
Gclifetime int64
@@ -44,12 +43,16 @@ func loadSessionFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("session")
SessionConfig.Provider = sec.Key("PROVIDER").In("memory",
[]string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
- SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(path.Join(AppDataPath, "sessions")), "\" ")
+ SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
- SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig)
+ SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
+ checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig)
}
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
+ if SessionConfig.CookiePath == "" {
+ SessionConfig.CookiePath = "/"
+ }
SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://"))
SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400)
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index d444d9a017..92bb0b6541 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/user"
+ "code.gitea.io/gitea/modules/util"
)
// settings
@@ -90,9 +91,9 @@ func PrepareAppDataPath() error {
return nil
}
-func InitCfgProvider(file string, extraConfigs ...string) {
+func InitCfgProvider(file string) {
var err error
- if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil {
+ if CfgProvider, err = NewConfigProviderFromFile(file); err != nil {
log.Fatal("Unable to init config provider from %q: %v", file, err)
}
CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls
@@ -158,9 +159,11 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
func loadRunModeFrom(rootCfg ConfigProvider) {
rootSec := rootCfg.Section("")
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
+
// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
+ unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || util.OptionalBoolParse(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
RunMode = os.Getenv("GITEA_RUN_MODE")
if RunMode == "" {
RunMode = rootSec.Key("RUN_MODE").MustString("prod")
@@ -226,3 +229,13 @@ func LoadSettingsForInstall() {
loadServiceFrom(CfgProvider)
loadMailerFrom(CfgProvider)
}
+
+var configuredPaths = make(map[string]string)
+
+func checkOverlappedPath(name, path string) {
+ // TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path)
+ if targetName, ok := configuredPaths[path]; ok && targetName != name {
+ logStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name)
+ }
+ configuredPaths[path] = name
+}
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index f937c7cff3..aeb61ac513 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -240,6 +240,8 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
}
}
+ checkOverlappedPath("[storage."+name+"].PATH", storage.Path)
+
return &storage, nil
}
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 34eae69329..16242d18ad 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -6,6 +6,7 @@ package structs
import (
"fmt"
"path"
+ "slices"
"strings"
"time"
@@ -141,12 +142,37 @@ const (
// IssueFormField represents a form field
// swagger:model
type IssueFormField struct {
- Type IssueFormFieldType `json:"type" yaml:"type"`
- ID string `json:"id" yaml:"id"`
- Attributes map[string]any `json:"attributes" yaml:"attributes"`
- Validations map[string]any `json:"validations" yaml:"validations"`
+ Type IssueFormFieldType `json:"type" yaml:"type"`
+ ID string `json:"id" yaml:"id"`
+ Attributes map[string]any `json:"attributes" yaml:"attributes"`
+ Validations map[string]any `json:"validations" yaml:"validations"`
+ Visible []IssueFormFieldVisible `json:"visible,omitempty"`
}
+func (iff IssueFormField) VisibleOnForm() bool {
+ if len(iff.Visible) == 0 {
+ return true
+ }
+ return slices.Contains(iff.Visible, IssueFormFieldVisibleForm)
+}
+
+func (iff IssueFormField) VisibleInContent() bool {
+ if len(iff.Visible) == 0 {
+ // we have our markdown exception
+ return iff.Type != IssueFormFieldTypeMarkdown
+ }
+ return slices.Contains(iff.Visible, IssueFormFieldVisibleContent)
+}
+
+// IssueFormFieldVisible defines issue form field visible
+// swagger:model
+type IssueFormFieldVisible string
+
+const (
+ IssueFormFieldVisibleForm IssueFormFieldVisible = "form"
+ IssueFormFieldVisibleContent IssueFormFieldVisible = "content"
+)
+
// IssueTemplate represents an issue template for a repository
// swagger:model
type IssueTemplate struct {
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 51e175fba8..bc8eb0b756 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -90,6 +90,7 @@ type Repository struct {
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
HasPullRequests bool `json:"has_pull_requests"`
HasProjects bool `json:"has_projects"`
+ ProjectsMode string `json:"projects_mode"`
HasReleases bool `json:"has_releases"`
HasPackages bool `json:"has_packages"`
HasActions bool `json:"has_actions"`
@@ -98,6 +99,7 @@ type Repository struct {
AllowRebase bool `json:"allow_rebase"`
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
AllowSquash bool `json:"allow_squash_merge"`
+ AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"`
AllowRebaseUpdate bool `json:"allow_rebase_update"`
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
DefaultMergeStyle string `json:"default_merge_style"`
@@ -179,6 +181,8 @@ type EditRepoOption struct {
HasPullRequests *bool `json:"has_pull_requests,omitempty"`
// either `true` to enable project unit, or `false` to disable them.
HasProjects *bool `json:"has_projects,omitempty"`
+ // `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
+ ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
// either `true` to enable releases unit, or `false` to disable them.
HasReleases *bool `json:"has_releases,omitempty"`
// either `true` to enable packages unit, or `false` to disable them.
@@ -195,6 +199,8 @@ type EditRepoOption struct {
AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"`
// either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging.
AllowSquash *bool `json:"allow_squash_merge,omitempty"`
+ // either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.
+ AllowFastForwardOnly *bool `json:"allow_fast_forward_only_merge,omitempty"`
// either `true` to allow mark pr as merged manually, or `false` to prevent it.
AllowManualMerge *bool `json:"allow_manual_merge,omitempty"`
// either `true` to enable AutodetectManualMerge, or `false` to prevent it. Note: In some special cases, misjudgments can occur.
@@ -203,7 +209,7 @@ type EditRepoOption struct {
AllowRebaseUpdate *bool `json:"allow_rebase_update,omitempty"`
// set to `true` to delete pr branch after merge by default
DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
- // set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash".
+ // set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", "squash", or "fast-forward-only".
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
// set to `true` to allow edits from maintainers by default
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
diff --git a/modules/structs/user.go b/modules/structs/user.go
index 0df67894b0..21ecc1479e 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -1,4 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
@@ -108,3 +109,26 @@ type UpdateUserAvatarOption struct {
// image must be base64 encoded
Image string `json:"image" binding:"Required"`
}
+
+// Badge represents a user badge
+// swagger:model
+type Badge struct {
+ ID int64 `json:"id"`
+ Slug string `json:"slug"`
+ Description string `json:"description"`
+ ImageURL string `json:"image_url"`
+}
+
+// UserBadge represents a user badge
+// swagger:model
+type UserBadge struct {
+ ID int64 `json:"id"`
+ BadgeID int64 `json:"badge_id"`
+ UserID int64 `json:"user_id"`
+}
+
+// UserBadgeOption options for link between users and badges
+type UserBadgeOption struct {
+ // example: ["badge1","badge2"]
+ BadgeSlugs []string `json:"badge_slugs" binding:"Required"`
+}
diff --git a/modules/structs/variable.go b/modules/structs/variable.go
new file mode 100644
index 0000000000..cc846cf0ec
--- /dev/null
+++ b/modules/structs/variable.go
@@ -0,0 +1,37 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package structs
+
+// CreateVariableOption the option when creating variable
+// swagger:model
+type CreateVariableOption struct {
+ // Value of the variable to create
+ //
+ // required: true
+ Value string `json:"value" binding:"Required"`
+}
+
+// UpdateVariableOption the option when updating variable
+// swagger:model
+type UpdateVariableOption struct {
+ // New name for the variable. If the field is empty, the variable name won't be updated.
+ Name string `json:"name"`
+ // Value of the variable to update
+ //
+ // required: true
+ Value string `json:"value" binding:"Required"`
+}
+
+// ActionVariable return value of the query API
+// swagger:model
+type ActionVariable struct {
+ // the owner to which the variable belongs
+ OwnerID int64 `json:"owner_id"`
+ // the repository to which the variable belongs
+ RepoID int64 `json:"repo_id"`
+ // the name of the variable
+ Name string `json:"name"`
+ // the value of the variable
+ Data string `json:"data"`
+}
diff --git a/modules/svg/svg.go b/modules/svg/svg.go
index 016e1dc08b..8132978cac 100644
--- a/modules/svg/svg.go
+++ b/modules/svg/svg.go
@@ -41,6 +41,21 @@ func Init() error {
return nil
}
+func MockIcon(icon string) func() {
+ if svgIcons == nil {
+ svgIcons = make(map[string]string)
+ }
+ orig, exist := svgIcons[icon]
+ svgIcons[icon] = fmt.Sprintf(``, icon, defaultSize, defaultSize)
+ return func() {
+ if exist {
+ svgIcons[icon] = orig
+ } else {
+ delete(svgIcons, icon)
+ }
+ }
+}
+
// RenderHTML renders icons - arguments icon name (string), size (int), class (string)
func RenderHTML(icon string, others ...any) template.HTML {
size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...)
@@ -55,5 +70,6 @@ func RenderHTML(icon string, others ...any) template.HTML {
}
return template.HTML(svgStr)
}
- return ""
+ // during test (or something wrong happens), there is no SVG loaded, so use a dummy span to tell that the icon is missing
+ return template.HTML(fmt.Sprintf("%s(%d/%s)", template.HTMLEscapeString(icon), size, template.HTMLEscapeString(class)))
}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 96cdd9ca46..5d2fa79bc5 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -9,12 +9,12 @@ import (
"html"
"html/template"
"net/url"
+ "slices"
"strings"
"time"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
@@ -33,15 +33,16 @@ func NewFuncMap() template.FuncMap {
// -----------------------------------------------------------------
// html/template related functions
- "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
- "Eval": Eval,
- "Safe": Safe,
- "Escape": html.EscapeString,
- "QueryEscape": url.QueryEscape,
- "JSEscape": template.JSEscapeString,
- "Str2html": Str2html, // TODO: rename it to SanitizeHTML
- "URLJoin": util.URLJoin,
- "DotEscape": DotEscape,
+ "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
+ "Eval": Eval,
+ "SafeHTML": SafeHTML,
+ "HTMLFormat": HTMLFormat,
+ "HTMLEscape": HTMLEscape,
+ "QueryEscape": QueryEscape,
+ "JSEscape": JSEscapeSafe,
+ "SanitizeHTML": SanitizeHTML,
+ "URLJoin": util.URLJoin,
+ "DotEscape": DotEscape,
"PathEscape": url.PathEscape,
"PathEscapeSegments": util.PathEscapeSegments,
@@ -52,13 +53,13 @@ func NewFuncMap() template.FuncMap {
"JsonUtils": NewJsonUtils,
// -----------------------------------------------------------------
- // svg / avatar / icon
+ // svg / avatar / icon / color
"svg": svg.RenderHTML,
"EntryIcon": base.EntryIcon,
"MigrationIcon": MigrationIcon,
"ActionIcon": ActionIcon,
-
- "SortArrow": SortArrow,
+ "SortArrow": SortArrow,
+ "ContrastColor": util.ContrastColor,
// -----------------------------------------------------------------
// time / number / format
@@ -105,6 +106,9 @@ func NewFuncMap() template.FuncMap {
"ShowFooterTemplateLoadTime": func() bool {
return setting.Other.ShowFooterTemplateLoadTime
},
+ "ShowFooterPoweredBy": func() bool {
+ return setting.Other.ShowFooterPoweredBy
+ },
"AllowedReactions": func() []string {
return setting.UI.Reactions
},
@@ -159,7 +163,6 @@ func NewFuncMap() template.FuncMap {
"RenderCodeBlock": RenderCodeBlock,
"RenderIssueTitle": RenderIssueTitle,
"RenderEmoji": RenderEmoji,
- "RenderEmojiPlain": emoji.ReplaceAliases,
"ReactionToEmoji": ReactionToEmoji,
"RenderMarkdownToHtml": RenderMarkdownToHtml,
@@ -179,14 +182,55 @@ func NewFuncMap() template.FuncMap {
}
}
-// Safe render raw as HTML
-func Safe(raw string) template.HTML {
- return template.HTML(raw)
+func HTMLFormat(s string, rawArgs ...any) template.HTML {
+ args := slices.Clone(rawArgs)
+ for i, v := range args {
+ switch v := v.(type) {
+ case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
+ // for most basic types (including template.HTML which is safe), just do nothing and use it
+ case string:
+ args[i] = template.HTMLEscapeString(v)
+ case fmt.Stringer:
+ args[i] = template.HTMLEscapeString(v.String())
+ default:
+ args[i] = template.HTMLEscapeString(fmt.Sprint(v))
+ }
+ }
+ return template.HTML(fmt.Sprintf(s, args...))
}
-// Str2html render Markdown text to HTML
-func Str2html(raw string) template.HTML {
- return template.HTML(markup.Sanitize(raw))
+// SafeHTML render raw as HTML
+func SafeHTML(s any) template.HTML {
+ switch v := s.(type) {
+ case string:
+ return template.HTML(v)
+ case template.HTML:
+ return v
+ }
+ panic(fmt.Sprintf("unexpected type %T", s))
+}
+
+// SanitizeHTML sanitizes the input by pre-defined markdown rules
+func SanitizeHTML(s string) template.HTML {
+ return template.HTML(markup.Sanitize(s))
+}
+
+func HTMLEscape(s any) template.HTML {
+ switch v := s.(type) {
+ case string:
+ return template.HTML(html.EscapeString(v))
+ case template.HTML:
+ return v
+ }
+ panic(fmt.Sprintf("unexpected type %T", s))
+}
+
+func JSEscapeSafe(s string) template.HTML {
+ return template.HTML(template.JSEscapeString(s))
+}
+
+func QueryEscape(s string) template.URL {
+ return template.URL(url.QueryEscape(s))
}
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index ec83e9ac33..64f29d033e 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -4,6 +4,7 @@
package templates
import (
+ "html/template"
"testing"
"github.com/stretchr/testify/assert"
@@ -52,3 +53,15 @@ func TestSubjectBodySeparator(t *testing.T) {
"",
"Insuficient\n--\nSeparators")
}
+
+func TestJSEscapeSafe(t *testing.T) {
+ assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`))
+}
+
+func TestHTMLFormat(t *testing.T) {
+ assert.Equal(t, template.HTML("< < 1"), HTMLFormat("%s %s %d", "<", template.HTML("<"), 1))
+}
+
+func TestSanitizeHTML(t *testing.T) {
+ assert.Equal(t, template.HTML(`link xss inline`), SanitizeHTML(`link xss inline`))
+}
diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go
index 54d857a8f6..f1832cba0e 100644
--- a/modules/templates/mailer.go
+++ b/modules/templates/mailer.go
@@ -5,6 +5,7 @@ package templates
import (
"context"
+ "fmt"
"html/template"
"regexp"
"strings"
@@ -33,7 +34,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
}
}
-func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
+func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
// Split template into subject and body
var subjectContent []byte
bodyContent := content
@@ -42,14 +43,13 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
subjectContent = content[0:loc[0]]
bodyContent = content[loc[1]:]
}
- if _, err := stpl.New(name).
- Parse(string(subjectContent)); err != nil {
- log.Warn("Failed to parse template [%s/subject]: %v", name, err)
+ if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
+ return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
}
- if _, err := btpl.New(name).
- Parse(string(bodyContent)); err != nil {
- log.Warn("Failed to parse template [%s/body]: %v", name, err)
+ if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
+ return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
}
+ return nil
}
// Mailer provides the templates required for sending notification mails.
@@ -81,7 +81,13 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
if firstRun {
log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
}
- buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content)
+ if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
+ if firstRun {
+ log.Fatal("Failed to parse mail template, err: %v", err)
+ } else {
+ log.Error("Failed to parse mail template, err: %v", err)
+ }
+ }
}
}
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 1d9635410b..0b53965f25 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
)
@@ -40,7 +41,7 @@ func RenderCommitMessage(ctx context.Context, msg string, metas map[string]strin
if len(msgLines) == 0 {
return template.HTML("")
}
- return template.HTML(msgLines[0])
+ return RenderCodeBlock(template.HTML(msgLines[0]))
}
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
@@ -67,7 +68,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string,
log.Error("RenderCommitMessageSubject: %v", err)
return template.HTML("")
}
- return template.HTML(renderedMessage)
+ return RenderCodeBlock(template.HTML(renderedMessage))
}
// RenderCommitBody extracts the body of a commit message without its title.
@@ -118,22 +119,25 @@ func RenderIssueTitle(ctx context.Context, text string, metas map[string]string)
}
// RenderLabel renders a label
-func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
- labelScope := label.ExclusiveScope()
-
- textColor := "#111"
- r, g, b := util.HexToRBGColor(label.Color)
- // Determine if label text should be light or dark to be readable on background color
- if util.UseLightTextOnBackground(r, g, b) {
- textColor = "#eee"
- }
+// locale is needed due to an import cycle with our context providing the `Tr` function
+func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML {
+ var (
+ archivedCSSClass string
+ textColor = util.ContrastColor(label.Color)
+ labelScope = label.ExclusiveScope()
+ )
description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
+ if label.IsArchived() {
+ archivedCSSClass = "archived-label"
+ description = fmt.Sprintf("(%s) %s", locale.TrString("archived"), description)
+ }
+
if labelScope == "" {
// Regular label
- s := fmt.Sprintf("%s",
- textColor, label.Color, description, RenderEmoji(ctx, label.Name))
+ s := fmt.Sprintf("%s",
+ archivedCSSClass, textColor, label.Color, description, RenderEmoji(ctx, label.Name))
return template.HTML(s)
}
@@ -143,7 +147,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
// Make scope and item background colors slightly darker and lighter respectively.
// More contrast needed with higher luminance, empirically tweaked.
- luminance := util.GetLuminance(r, g, b)
+ luminance := util.GetRelativeLuminance(label.Color)
contrast := 0.01 + luminance*0.03
// Ensure we add the same amount of contrast also near 0 and 1.
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
@@ -152,6 +156,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
+ r, g, b := util.HexToRBGColor(label.Color)
scopeBytes := []byte{
uint8(math.Min(math.Round(r*darkenFactor), 255)),
uint8(math.Min(math.Round(g*darkenFactor), 255)),
@@ -166,11 +171,11 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
itemColor := "#" + hex.EncodeToString(itemBytes)
scopeColor := "#" + hex.EncodeToString(scopeBytes)
- s := fmt.Sprintf(""+
+ s := fmt.Sprintf(""+
"%s"+
"%s"+
"",
- description,
+ archivedCSSClass, description,
textColor, scopeColor, scopeText,
textColor, itemColor, itemText)
return template.HTML(s)
@@ -208,10 +213,10 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n
if err != nil {
log.Error("RenderString: %v", err)
}
- return template.HTML(output)
+ return output
}
-func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
+func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string) template.HTML {
htmlCode := ``
for _, label := range labels {
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
@@ -219,7 +224,7 @@ func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink st
continue
}
htmlCode += fmt.Sprintf("%s ",
- repoLink, label.ID, RenderLabel(ctx, label))
+ repoLink, label.ID, RenderLabel(ctx, locale, label))
}
htmlCode += ""
return template.HTML(htmlCode)
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index 8648967d38..15aee8912d 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -117,21 +117,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
-@mention-user test
-#123
+@mention-user test
+#123
space`
assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
}
func TestRenderCommitMessage(t *testing.T) {
- expected := `space @mention-user `
+ expected := `space @mention-user `
assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas))
}
func TestRenderCommitMessageLinkSubject(t *testing.T) {
- expected := `space @mention-user`
+ expected := `space @mention-user`
assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas))
}
@@ -155,14 +155,14 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
@mention-user test
-#123
+#123
space
`
assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
}
func TestRenderMarkdownToHtml(t *testing.T) {
- expected := `space @mention-user
+ expected := `
space @mention-user
/just/a/path.bin
https://example.com/file.bin
local link
@@ -179,7 +179,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
👍
mail@domain.com
-@mention-user test
+@mention-user test
#123
space
`
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 2771b1e223..479b755da1 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -4,6 +4,8 @@
package templates
import (
+ "fmt"
+ "html/template"
"strings"
"code.gitea.io/gitea/modules/base"
@@ -17,6 +19,19 @@ func NewStringUtils() *StringUtils {
return &stringUtils
}
+func (su *StringUtils) ToString(v any) string {
+ switch v := v.(type) {
+ case string:
+ return v
+ case template.HTML:
+ return string(v)
+ case fmt.Stringer:
+ return v.String()
+ default:
+ return fmt.Sprint(v)
+ }
+}
+
func (su *StringUtils) HasPrefix(s, prefix string) bool {
return strings.HasPrefix(s, prefix)
}
diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go
index 62b94f7cf4..c089173560 100644
--- a/modules/timeutil/datetime.go
+++ b/modules/timeutil/datetime.go
@@ -13,6 +13,8 @@ import (
// DateTime renders an absolute time HTML element by datetime.
func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
+ // TODO: remove the extraAttrs argument, it's not used in any call to DateTime
+
if p, ok := datetime.(*time.Time); ok {
datetime = *p
}
@@ -51,18 +53,16 @@ func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
attrs := make([]string, 0, 10+len(extraAttrs))
attrs = append(attrs, extraAttrs...)
- attrs = append(attrs, `data-tooltip-content`, `data-tooltip-interactive="true"`)
- attrs = append(attrs, `format="datetime"`, `weekday=""`, `year="numeric"`)
+ attrs = append(attrs, `weekday=""`, `year="numeric"`)
switch format {
- case "short":
- attrs = append(attrs, `month="short"`, `day="numeric"`)
- case "long":
- attrs = append(attrs, `month="long"`, `day="numeric"`)
- case "full":
- attrs = append(attrs, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`)
+ case "short", "long": // date only
+ attrs = append(attrs, `month="`+format+`"`, `day="numeric"`)
+ return template.HTML(fmt.Sprintf(`%s `, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
+ case "full": // full date including time
+ attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
+ return template.HTML(fmt.Sprintf(`%s `, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
default:
panic(fmt.Sprintf("Unsupported format %s", format))
}
- return template.HTML(fmt.Sprintf(`%s `, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
}
diff --git a/modules/timeutil/datetime_test.go b/modules/timeutil/datetime_test.go
index 26494b8475..ac2ce35ba2 100644
--- a/modules/timeutil/datetime_test.go
+++ b/modules/timeutil/datetime_test.go
@@ -18,6 +18,7 @@ func TestDateTime(t *testing.T) {
defer test.MockVariableValue(&setting.DefaultUILocation, testTz)()
refTimeStr := "2018-01-01T00:00:00Z"
+ refDateStr := "2018-01-01"
refTime, _ := time.Parse(time.RFC3339, refTimeStr)
refTimeStamp := TimeStamp(refTime.Unix())
@@ -27,17 +28,20 @@ func TestDateTime(t *testing.T) {
assert.EqualValues(t, "-", DateTime("short", TimeStamp(0)))
actual := DateTime("short", "invalid")
- assert.EqualValues(t, `invalid `, actual)
+ assert.EqualValues(t, `invalid `, actual)
actual = DateTime("short", refTimeStr)
- assert.EqualValues(t, `2018-01-01T00:00:00Z `, actual)
+ assert.EqualValues(t, `2018-01-01T00:00:00Z `, actual)
actual = DateTime("short", refTime)
- assert.EqualValues(t, `2018-01-01 `, actual)
+ assert.EqualValues(t, `2018-01-01 `, actual)
+
+ actual = DateTime("short", refDateStr)
+ assert.EqualValues(t, `2018-01-01 `, actual)
actual = DateTime("short", refTimeStamp)
- assert.EqualValues(t, `2017-12-31 `, actual)
+ assert.EqualValues(t, `2017-12-31 `, actual)
actual = DateTime("full", refTimeStamp)
- assert.EqualValues(t, `2017-12-31 19:00:00 -05:00 `, actual)
+ assert.EqualValues(t, `2017-12-31 19:00:00 -05:00 `, actual)
}
diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go
index 1cb3c4f288..dba42c793a 100644
--- a/modules/timeutil/since.go
+++ b/modules/timeutil/since.go
@@ -28,54 +28,54 @@ func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) {
switch {
case diff <= 0:
diff = 0
- diffStr = lang.Tr("tool.now")
+ diffStr = lang.TrString("tool.now")
case diff < 2:
diff = 0
- diffStr = lang.Tr("tool.1s")
+ diffStr = lang.TrString("tool.1s")
case diff < 1*Minute:
- diffStr = lang.Tr("tool.seconds", diff)
+ diffStr = lang.TrString("tool.seconds", diff)
diff = 0
case diff < 2*Minute:
diff -= 1 * Minute
- diffStr = lang.Tr("tool.1m")
+ diffStr = lang.TrString("tool.1m")
case diff < 1*Hour:
- diffStr = lang.Tr("tool.minutes", diff/Minute)
+ diffStr = lang.TrString("tool.minutes", diff/Minute)
diff -= diff / Minute * Minute
case diff < 2*Hour:
diff -= 1 * Hour
- diffStr = lang.Tr("tool.1h")
+ diffStr = lang.TrString("tool.1h")
case diff < 1*Day:
- diffStr = lang.Tr("tool.hours", diff/Hour)
+ diffStr = lang.TrString("tool.hours", diff/Hour)
diff -= diff / Hour * Hour
case diff < 2*Day:
diff -= 1 * Day
- diffStr = lang.Tr("tool.1d")
+ diffStr = lang.TrString("tool.1d")
case diff < 1*Week:
- diffStr = lang.Tr("tool.days", diff/Day)
+ diffStr = lang.TrString("tool.days", diff/Day)
diff -= diff / Day * Day
case diff < 2*Week:
diff -= 1 * Week
- diffStr = lang.Tr("tool.1w")
+ diffStr = lang.TrString("tool.1w")
case diff < 1*Month:
- diffStr = lang.Tr("tool.weeks", diff/Week)
+ diffStr = lang.TrString("tool.weeks", diff/Week)
diff -= diff / Week * Week
case diff < 2*Month:
diff -= 1 * Month
- diffStr = lang.Tr("tool.1mon")
+ diffStr = lang.TrString("tool.1mon")
case diff < 1*Year:
- diffStr = lang.Tr("tool.months", diff/Month)
+ diffStr = lang.TrString("tool.months", diff/Month)
diff -= diff / Month * Month
case diff < 2*Year:
diff -= 1 * Year
- diffStr = lang.Tr("tool.1y")
+ diffStr = lang.TrString("tool.1y")
default:
- diffStr = lang.Tr("tool.years", diff/Year)
+ diffStr = lang.TrString("tool.years", diff/Year)
diff -= (diff / Year) * Year
}
return diff, diffStr
@@ -97,10 +97,10 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
diff := now.Unix() - then.Unix()
if then.After(now) {
- return lang.Tr("tool.future")
+ return lang.TrString("tool.future")
}
if diff == 0 {
- return lang.Tr("tool.now")
+ return lang.TrString("tool.now")
}
var timeStr, diffStr string
@@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
return strings.TrimPrefix(timeStr, ", ")
}
-func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
+func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
friendlyText := then.Format("2006-01-02 15:04:05 -07:00")
// document: https://github.com/github/relative-time-element
@@ -126,7 +126,7 @@ func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
}
// declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip
- htm := fmt.Sprintf(`%s `,
+ htm := fmt.Sprintf(`%s `,
attrs, then.Format(time.RFC3339), friendlyText)
return template.HTML(htm)
}
@@ -134,7 +134,7 @@ func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
// TimeSince renders relative time HTML given a time.Time
func TimeSince(then time.Time, lang translation.Locale) template.HTML {
if setting.UI.PreferredTimestampTense == "absolute" {
- return DateTime("full", then, `class="time-since"`)
+ return DateTime("full", then)
}
return timeSinceUnix(then, time.Now(), lang)
}
diff --git a/modules/timeutil/timestamp.go b/modules/timeutil/timestamp.go
index 27a80b6682..e77652b24f 100644
--- a/modules/timeutil/timestamp.go
+++ b/modules/timeutil/timestamp.go
@@ -21,8 +21,9 @@ var (
)
// MockSet sets the time to a mocked time.Time
-func MockSet(now time.Time) {
+func MockSet(now time.Time) func() {
mockNow = now
+ return MockUnset
}
// MockUnset will unset the mocked time.Time
diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go
index 42475545b3..1555cd961e 100644
--- a/modules/translation/i18n/i18n.go
+++ b/modules/translation/i18n/i18n.go
@@ -4,26 +4,25 @@
package i18n
import (
+ "html/template"
"io"
)
var DefaultLocales = NewLocaleStore()
type Locale interface {
- // Tr translates a given key and arguments for a language
- Tr(trKey string, trArgs ...any) string
- // Has reports if a locale has a translation for a given key
- Has(trKey string) bool
+ // TrString translates a given key and arguments for a language
+ TrString(trKey string, trArgs ...any) string
+ // TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
+ TrHTML(trKey string, trArgs ...any) template.HTML
+ // HasKey reports if a locale has a translation for a given key
+ HasKey(trKey string) bool
}
// LocaleStore provides the functions common to all locale stores
type LocaleStore interface {
io.Closer
- // Tr translates a given key and arguments for a language
- Tr(lang, trKey string, trArgs ...any) string
- // Has reports if a locale has a translation for a given key
- Has(lang, trKey string) bool
// SetDefaultLang sets the default language to fall back to
SetDefaultLang(lang string)
// ListLangNameDesc provides paired slices of language names to descriptors
@@ -45,7 +44,7 @@ func ResetDefaultLocales() {
DefaultLocales = NewLocaleStore()
}
-// GetLocales returns the locale from the default locales
+// GetLocale returns the locale from the default locales
func GetLocale(lang string) (Locale, bool) {
return DefaultLocales.Locale(lang)
}
diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go
index 1d1be43318..b364992dfe 100644
--- a/modules/translation/i18n/i18n_test.go
+++ b/modules/translation/i18n/i18n_test.go
@@ -4,6 +4,7 @@
package i18n
import (
+ "html/template"
"strings"
"testing"
@@ -17,7 +18,7 @@ fmt = %[1]s %[2]s
[section]
sub = Sub String
-mixed = test value; more text
+mixed = test value; %s
`)
testData2 := []byte(`
@@ -32,29 +33,33 @@ sub = Changed Sub String
assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
ls.SetDefaultLang("lang1")
- result := ls.Tr("lang1", "fmt", "a", "b")
+ lang1, _ := ls.Locale("lang1")
+ lang2, _ := ls.Locale("lang2")
+
+ result := lang1.TrString("fmt", "a", "b")
assert.Equal(t, "a b", result)
- result = ls.Tr("lang2", "fmt", "a", "b")
+ result = lang2.TrString("fmt", "a", "b")
assert.Equal(t, "b a", result)
- result = ls.Tr("lang1", "section.sub")
+ result = lang1.TrString("section.sub")
assert.Equal(t, "Sub String", result)
- result = ls.Tr("lang2", "section.sub")
+ result = lang2.TrString("section.sub")
assert.Equal(t, "Changed Sub String", result)
- result = ls.Tr("", ".dot.name")
+ langNone, _ := ls.Locale("none")
+ result = langNone.TrString(".dot.name")
assert.Equal(t, "Dot Name", result)
- result = ls.Tr("lang2", "section.mixed")
- assert.Equal(t, `test value; more text`, result)
+ result2 := lang2.TrHTML("section.mixed", "a&b")
+ assert.EqualValues(t, `test value; a&b`, result2)
langs, descs := ls.ListLangNameDesc()
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
- found := ls.Has("lang1", "no-such")
+ found := lang1.HasKey("no-such")
assert.False(t, found)
assert.NoError(t, ls.Close())
}
@@ -72,9 +77,75 @@ c=22
ls := NewLocaleStore()
assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
- assert.Equal(t, "11", ls.Tr("lang1", "a"))
- assert.Equal(t, "21", ls.Tr("lang1", "b"))
- assert.Equal(t, "22", ls.Tr("lang1", "c"))
+ lang1, _ := ls.Locale("lang1")
+ assert.Equal(t, "11", lang1.TrString("a"))
+ assert.Equal(t, "21", lang1.TrString("b"))
+ assert.Equal(t, "22", lang1.TrString("c"))
+}
+
+type stringerPointerReceiver struct {
+ s string
+}
+
+func (s *stringerPointerReceiver) String() string {
+ return s.s
+}
+
+type stringerStructReceiver struct {
+ s string
+}
+
+func (s stringerStructReceiver) String() string {
+ return s.s
+}
+
+type errorStructReceiver struct {
+ s string
+}
+
+func (e errorStructReceiver) Error() string {
+ return e.s
+}
+
+type errorPointerReceiver struct {
+ s string
+}
+
+func (e *errorPointerReceiver) Error() string {
+ return e.s
+}
+
+func TestLocaleWithTemplate(t *testing.T) {
+ ls := NewLocaleStore()
+ assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=%s`), nil))
+ lang1, _ := ls.Locale("lang1")
+
+ tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
+ tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`))
+
+ cases := []struct {
+ in any
+ want string
+ }{
+ {"", "<str>"},
+ {[]byte(""), "[60 98 121 116 101 115 62]"},
+ {template.HTML(""), ""},
+ {stringerPointerReceiver{""}, "{<stringerPointerReceiver>}"},
+ {&stringerPointerReceiver{""}, "<stringerPointerReceiver ptr>"},
+ {stringerStructReceiver{""}, "<stringerStructReceiver>"},
+ {&stringerStructReceiver{""}, "<stringerStructReceiver ptr>"},
+ {errorStructReceiver{""}, "<errorStructReceiver>"},
+ {&errorStructReceiver{""}, "<errorStructReceiver ptr>"},
+ {errorPointerReceiver{""}, "{<errorPointerReceiver>}"},
+ {&errorPointerReceiver{""}, "<errorPointerReceiver ptr>"},
+ }
+
+ buf := &strings.Builder{}
+ for _, c := range cases {
+ buf.Reset()
+ assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in}))
+ assert.Equal(t, c.want, buf.String())
+ }
}
func TestLocaleStoreQuirks(t *testing.T) {
@@ -110,8 +181,9 @@ func TestLocaleStoreQuirks(t *testing.T) {
for _, testData := range testDataList {
ls := NewLocaleStore()
err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
+ lang1, _ := ls.Locale("lang1")
assert.NoError(t, err, testData.hint)
- assert.Equal(t, testData.out, ls.Tr("lang1", "a"), testData.hint)
+ assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
assert.NoError(t, ls.Close())
}
diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go
index f5a951a79f..b422996984 100644
--- a/modules/translation/i18n/localestore.go
+++ b/modules/translation/i18n/localestore.go
@@ -5,6 +5,8 @@ package i18n
import (
"fmt"
+ "html/template"
+ "slices"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -18,6 +20,8 @@ type locale struct {
idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
}
+var _ Locale = (*locale)(nil)
+
type localeStore struct {
// After initializing has finished, these fields are read-only.
langNames []string
@@ -85,20 +89,6 @@ func (store *localeStore) SetDefaultLang(lang string) {
store.defaultLang = lang
}
-// Tr translates content to target language. fall back to default language.
-func (store *localeStore) Tr(lang, trKey string, trArgs ...any) string {
- l, _ := store.Locale(lang)
-
- return l.Tr(trKey, trArgs...)
-}
-
-// Has returns whether the given language has a translation for the provided key
-func (store *localeStore) Has(lang, trKey string) bool {
- l, _ := store.Locale(lang)
-
- return l.Has(trKey)
-}
-
// Locale returns the locale for the lang or the default language
func (store *localeStore) Locale(lang string) (Locale, bool) {
l, found := store.localeMap[lang]
@@ -113,13 +103,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
return l, found
}
-// Close implements io.Closer
func (store *localeStore) Close() error {
return nil
}
-// Tr translates content to locale language. fall back to default language.
-func (l *locale) Tr(trKey string, trArgs ...any) string {
+func (l *locale) TrString(trKey string, trArgs ...any) string {
format := trKey
idx, ok := l.store.trKeyToIdxMap[trKey]
@@ -141,8 +129,25 @@ func (l *locale) Tr(trKey string, trArgs ...any) string {
return msg
}
-// Has returns whether a key is present in this locale or not
-func (l *locale) Has(trKey string) bool {
+func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
+ args := slices.Clone(trArgs)
+ for i, v := range args {
+ switch v := v.(type) {
+ case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
+ // for most basic types (including template.HTML which is safe), just do nothing and use it
+ case string:
+ args[i] = template.HTMLEscapeString(v)
+ case fmt.Stringer:
+ args[i] = template.HTMLEscapeString(v.String())
+ default:
+ args[i] = template.HTMLEscapeString(fmt.Sprint(v))
+ }
+ }
+ return template.HTML(l.TrString(trKey, args...))
+}
+
+// HasKey returns whether a key is present in this locale or not
+func (l *locale) HasKey(trKey string) bool {
idx, ok := l.store.trKeyToIdxMap[trKey]
if !ok {
return false
diff --git a/modules/translation/mock.go b/modules/translation/mock.go
index 2d0cb17324..f457271ea5 100644
--- a/modules/translation/mock.go
+++ b/modules/translation/mock.go
@@ -3,10 +3,16 @@
package translation
-import "fmt"
+import (
+ "fmt"
+ "html/template"
+ "strings"
+)
// MockLocale provides a mocked locale without any translations
-type MockLocale struct{}
+type MockLocale struct {
+ Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
+}
var _ Locale = (*MockLocale)(nil)
@@ -14,14 +20,25 @@ func (l MockLocale) Language() string {
return "en"
}
-func (l MockLocale) Tr(s string, _ ...any) string {
- return s
+func (l MockLocale) TrString(s string, args ...any) string {
+ return sprintAny(s, args...)
}
-func (l MockLocale) TrN(_cnt any, key1, _keyN string, _args ...any) string {
- return key1
+func (l MockLocale) Tr(s string, args ...any) template.HTML {
+ return template.HTML(sprintAny(s, args...))
+}
+
+func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
+ return template.HTML(sprintAny(key1, args...))
}
func (l MockLocale) PrettyNumber(v any) string {
return fmt.Sprint(v)
}
+
+func sprintAny(s string, args ...any) string {
+ if len(args) == 0 {
+ return s
+ }
+ return s + ":" + fmt.Sprintf(strings.Repeat(",%v", len(args))[1:], args...)
+}
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index dba4de6607..36ae58a9f1 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -5,6 +5,7 @@ package translation
import (
"context"
+ "html/template"
"sort"
"strings"
"sync"
@@ -27,8 +28,11 @@ var ContextKey any = &contextKey{}
// Locale represents an interface to translation
type Locale interface {
Language() string
- Tr(string, ...any) string
- TrN(cnt any, key1, keyN string, args ...any) string
+ TrString(string, ...any) string
+
+ Tr(key string, args ...any) template.HTML
+ TrN(cnt any, key1, keyN string, args ...any) template.HTML
+
PrettyNumber(v any) string
}
@@ -140,10 +144,12 @@ func Match(tags ...language.Tag) language.Tag {
// locale represents the information of localization.
type locale struct {
i18n.Locale
- Lang, LangName string // these fields are used directly in templates: .i18n.Lang
+ Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
msgPrinter *message.Printer
}
+var _ Locale = (*locale)(nil)
+
// NewLocale return a locale
func NewLocale(lang string) Locale {
if lock != nil {
@@ -216,8 +222,12 @@ var trNLangRules = map[string]func(int64) int{
},
}
+func (l *locale) Tr(s string, args ...any) template.HTML {
+ return l.TrHTML(s, args...)
+}
+
// TrN returns translated message for plural text translation
-func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string {
+func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
var c int64
if t, ok := cnt.(int); ok {
c = int64(t)
diff --git a/modules/util/color.go b/modules/util/color.go
index 240b045c28..9c520dce78 100644
--- a/modules/util/color.go
+++ b/modules/util/color.go
@@ -4,22 +4,10 @@ package util
import (
"fmt"
- "math"
"strconv"
"strings"
)
-// Check similar implementation in web_src/js/utils/color.js and keep synchronization
-
-// Return R, G, B values defined in reletive luminance
-func getLuminanceRGB(channel float64) float64 {
- sRGB := channel / 255
- if sRGB <= 0.03928 {
- return sRGB / 12.92
- }
- return math.Pow((sRGB+0.055)/1.055, 2.4)
-}
-
// Get color as RGB values in 0..255 range from the hex color string (with or without #)
func HexToRBGColor(colorString string) (float64, float64, float64) {
hexString := colorString
@@ -47,19 +35,23 @@ func HexToRBGColor(colorString string) (float64, float64, float64) {
return r, g, b
}
-// return luminance given RGB channels
-// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance
-func GetLuminance(r, g, b float64) float64 {
- R := getLuminanceRGB(r)
- G := getLuminanceRGB(g)
- B := getLuminanceRGB(b)
- luminance := 0.2126*R + 0.7152*G + 0.0722*B
- return luminance
+// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
+// Keep this in sync with web_src/js/utils/color.js
+func GetRelativeLuminance(color string) float64 {
+ r, g, b := HexToRBGColor(color)
+ return (0.2126729*r + 0.7151522*g + 0.0721750*b) / 255
}
-// Reference from: https://firsching.ch/github_labels.html
-// In the future WCAG 3 APCA may be a better solution.
-// Check if text should use light color based on RGB of background
-func UseLightTextOnBackground(r, g, b float64) bool {
- return GetLuminance(r, g, b) < 0.453
+func UseLightText(backgroundColor string) bool {
+ return GetRelativeLuminance(backgroundColor) < 0.453
+}
+
+// Given a background color, returns a black or white foreground color that the highest
+// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
+// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
+func ContrastColor(backgroundColor string) string {
+ if UseLightText(backgroundColor) {
+ return "#fff"
+ }
+ return "#000"
}
diff --git a/modules/util/color_test.go b/modules/util/color_test.go
index d96ac36730..be6e6b122a 100644
--- a/modules/util/color_test.go
+++ b/modules/util/color_test.go
@@ -33,33 +33,31 @@ func Test_HexToRBGColor(t *testing.T) {
}
}
-func Test_UseLightTextOnBackground(t *testing.T) {
+func Test_UseLightText(t *testing.T) {
cases := []struct {
- r float64
- g float64
- b float64
- expected bool
+ color string
+ expected string
}{
- {215, 58, 74, true},
- {0, 117, 202, true},
- {207, 211, 215, false},
- {162, 238, 239, false},
- {112, 87, 255, true},
- {0, 134, 114, true},
- {228, 230, 105, false},
- {216, 118, 227, true},
- {255, 255, 255, false},
- {43, 134, 133, true},
- {43, 135, 134, true},
- {44, 135, 134, true},
- {59, 182, 179, true},
- {124, 114, 104, true},
- {126, 113, 108, true},
- {129, 112, 109, true},
- {128, 112, 112, true},
+ {"#d73a4a", "#fff"},
+ {"#0075ca", "#fff"},
+ {"#cfd3d7", "#000"},
+ {"#a2eeef", "#000"},
+ {"#7057ff", "#fff"},
+ {"#008672", "#fff"},
+ {"#e4e669", "#000"},
+ {"#d876e3", "#000"},
+ {"#ffffff", "#000"},
+ {"#2b8684", "#fff"},
+ {"#2b8786", "#fff"},
+ {"#2c8786", "#000"},
+ {"#3bb6b3", "#000"},
+ {"#7c7268", "#fff"},
+ {"#7e716c", "#fff"},
+ {"#81706d", "#fff"},
+ {"#807070", "#fff"},
+ {"#84b6eb", "#000"},
}
for n, c := range cases {
- result := UseLightTextOnBackground(c.r, c.g, c.b)
- assert.Equal(t, c.expected, result, "case %d: error should match", n)
+ assert.Equal(t, c.expected, ContrastColor(c.color), "case %d: error should match", n)
}
}
diff --git a/modules/util/filebuffer/file_backed_buffer.go b/modules/util/filebuffer/file_backed_buffer.go
index 6b07bd0413..739543e297 100644
--- a/modules/util/filebuffer/file_backed_buffer.go
+++ b/modules/util/filebuffer/file_backed_buffer.go
@@ -149,6 +149,7 @@ func (b *FileBackedBuffer) Close() error {
if b.file != nil {
err := b.file.Close()
os.Remove(b.file.Name())
+ b.file = nil
return err
}
return nil
diff --git a/modules/util/keypair.go b/modules/util/keypair.go
index 97f2d9ebca..8b86c142af 100644
--- a/modules/util/keypair.go
+++ b/modules/util/keypair.go
@@ -7,10 +7,9 @@ import (
"crypto"
"crypto/rand"
"crypto/rsa"
+ "crypto/sha256"
"crypto/x509"
"encoding/pem"
-
- "github.com/minio/sha256-simd"
)
// GenerateKeyPair generates a public and private keypair
diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go
index c9925f7988..c6f68c845a 100644
--- a/modules/util/keypair_test.go
+++ b/modules/util/keypair_test.go
@@ -7,12 +7,12 @@ import (
"crypto"
"crypto/rand"
"crypto/rsa"
+ "crypto/sha256"
"crypto/x509"
"encoding/pem"
"regexp"
"testing"
- "github.com/minio/sha256-simd"
"github.com/stretchr/testify/assert"
)
diff --git a/modules/util/slice.go b/modules/util/slice.go
index a7073fedee..9c878c24be 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -53,3 +53,21 @@ func Sorted[S ~[]E, E cmp.Ordered](values S) S {
slices.Sort(values)
return values
}
+
+// TODO: Replace with "maps.Values" once available, current it only in golang.org/x/exp/maps but not in standard library
+func ValuesOfMap[K comparable, V any](m map[K]V) []V {
+ values := make([]V, 0, len(m))
+ for _, v := range m {
+ values = append(values, v)
+ }
+ return values
+}
+
+// TODO: Replace with "maps.Keys" once available, current it only in golang.org/x/exp/maps but not in standard library
+func KeysOfMap[K comparable, V any](m map[K]V) []K {
+ keys := make([]K, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ return keys
+}
diff --git a/modules/util/util.go b/modules/util/util.go
index c47931f6c9..44b5a6ed81 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -6,58 +6,24 @@ package util
import (
"bytes"
"crypto/rand"
- "encoding/base64"
"fmt"
"math/big"
"strconv"
"strings"
+ "code.gitea.io/gitea/modules/optional"
+
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
-// OptionalBool a boolean that can be "null"
-type OptionalBool byte
-
-const (
- // OptionalBoolNone a "null" boolean value
- OptionalBoolNone OptionalBool = iota
- // OptionalBoolTrue a "true" boolean value
- OptionalBoolTrue
- // OptionalBoolFalse a "false" boolean value
- OptionalBoolFalse
-)
-
-// IsTrue return true if equal to OptionalBoolTrue
-func (o OptionalBool) IsTrue() bool {
- return o == OptionalBoolTrue
-}
-
-// IsFalse return true if equal to OptionalBoolFalse
-func (o OptionalBool) IsFalse() bool {
- return o == OptionalBoolFalse
-}
-
-// IsNone return true if equal to OptionalBoolNone
-func (o OptionalBool) IsNone() bool {
- return o == OptionalBoolNone
-}
-
-// OptionalBoolOf get the corresponding OptionalBool of a bool
-func OptionalBoolOf(b bool) OptionalBool {
- if b {
- return OptionalBoolTrue
- }
- return OptionalBoolFalse
-}
-
-// OptionalBoolParse get the corresponding OptionalBool of a string using strconv.ParseBool
-func OptionalBoolParse(s string) OptionalBool {
- b, e := strconv.ParseBool(s)
+// OptionalBoolParse get the corresponding optional.Option[bool] of a string using strconv.ParseBool
+func OptionalBoolParse(s string) optional.Option[bool] {
+ v, e := strconv.ParseBool(s)
if e != nil {
- return OptionalBoolNone
+ return optional.None[bool]()
}
- return OptionalBoolOf(b)
+ return optional.Some(v)
}
// IsEmptyString checks if the provided string is empty
@@ -247,12 +213,28 @@ func ToPointer[T any](val T) *T {
return &val
}
-func Base64FixedDecode(encoding *base64.Encoding, src []byte, length int) ([]byte, error) {
- decoded := make([]byte, encoding.DecodedLen(len(src))+3)
- if n, err := encoding.Decode(decoded, src); err != nil {
- return nil, err
- } else if n != length {
- return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, length)
+// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
+func Iif[T any](condition bool, trueVal, falseVal T) T {
+ if condition {
+ return trueVal
}
- return decoded[:length], nil
+ return falseVal
+}
+
+// IfZero returns "def" if "v" is a zero value, otherwise "v"
+func IfZero[T comparable](v, def T) T {
+ var zero T
+ if v == zero {
+ return def
+ }
+ return v
+}
+
+func ReserveLineBreakForTextarea(input string) string {
+ // Since the content is from a form which is a textarea, the line endings are \r\n.
+ // It's a standard behavior of HTML.
+ // But we want to store them as \n like what GitHub does.
+ // And users are unlikely to really need to keep the \r.
+ // Other than this, we should respect the original content, even leading or trailing spaces.
+ return strings.ReplaceAll(input, "\r\n", "\n")
}
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 8509d8aced..5c5b13d04b 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -4,11 +4,12 @@
package util
import (
- "encoding/base64"
"regexp"
"strings"
"testing"
+ "code.gitea.io/gitea/modules/optional"
+
"github.com/stretchr/testify/assert"
)
@@ -174,17 +175,17 @@ func Test_RandomBytes(t *testing.T) {
assert.NotEqual(t, bytes3, bytes4)
}
-func Test_OptionalBool(t *testing.T) {
- assert.Equal(t, OptionalBoolNone, OptionalBoolParse(""))
- assert.Equal(t, OptionalBoolNone, OptionalBoolParse("x"))
+func TestOptionalBoolParse(t *testing.T) {
+ assert.Equal(t, optional.None[bool](), OptionalBoolParse(""))
+ assert.Equal(t, optional.None[bool](), OptionalBoolParse("x"))
- assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("0"))
- assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("f"))
- assert.Equal(t, OptionalBoolFalse, OptionalBoolParse("False"))
+ assert.Equal(t, optional.Some(false), OptionalBoolParse("0"))
+ assert.Equal(t, optional.Some(false), OptionalBoolParse("f"))
+ assert.Equal(t, optional.Some(false), OptionalBoolParse("False"))
- assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("1"))
- assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("t"))
- assert.Equal(t, OptionalBoolTrue, OptionalBoolParse("True"))
+ assert.Equal(t, optional.Some(true), OptionalBoolParse("1"))
+ assert.Equal(t, optional.Some(true), OptionalBoolParse("t"))
+ assert.Equal(t, optional.Some(true), OptionalBoolParse("True"))
}
// Test case for any function which accepts and returns a single string.
@@ -235,15 +236,7 @@ func TestToPointer(t *testing.T) {
assert.False(t, &val123 == ToPointer(val123))
}
-func TestBase64FixedDecode(t *testing.T) {
- _, err := Base64FixedDecode(base64.RawURLEncoding, []byte("abcd"), 32)
- assert.ErrorContains(t, err, "invalid base64 decoded length")
- _, err = Base64FixedDecode(base64.RawURLEncoding, []byte(strings.Repeat("a", 64)), 32)
- assert.ErrorContains(t, err, "invalid base64 decoded length")
-
- str32 := strings.Repeat("x", 32)
- encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
- decoded32, err := Base64FixedDecode(base64.RawURLEncoding, []byte(encoded32), 32)
- assert.NoError(t, err)
- assert.Equal(t, str32, string(decoded32))
+func TestReserveLineBreakForTextarea(t *testing.T) {
+ assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata")
+ assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n")
}
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go
index d9bcdf3b2a..43e1bbc70e 100644
--- a/modules/web/middleware/binding.go
+++ b/modules/web/middleware/binding.go
@@ -104,40 +104,40 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
trName := field.Tag.Get("locale")
if len(trName) == 0 {
- trName = l.Tr("form." + field.Name)
+ trName = l.TrString("form." + field.Name)
} else {
- trName = l.Tr(trName)
+ trName = l.TrString(trName)
}
switch errs[0].Classification {
case binding.ERR_REQUIRED:
- data["ErrorMsg"] = trName + l.Tr("form.require_error")
+ data["ErrorMsg"] = trName + l.TrString("form.require_error")
case binding.ERR_ALPHA_DASH:
- data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
+ data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
case binding.ERR_ALPHA_DASH_DOT:
- data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
+ data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
case validation.ErrGitRefName:
- data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error")
+ data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
case binding.ERR_SIZE:
- data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field))
+ data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
case binding.ERR_MIN_SIZE:
- data["ErrorMsg"] = trName + l.Tr("form.min_size_error", GetMinSize(field))
+ data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
case binding.ERR_MAX_SIZE:
- data["ErrorMsg"] = trName + l.Tr("form.max_size_error", GetMaxSize(field))
+ data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
case binding.ERR_EMAIL:
- data["ErrorMsg"] = trName + l.Tr("form.email_error")
+ data["ErrorMsg"] = trName + l.TrString("form.email_error")
case binding.ERR_URL:
- data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message)
+ data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
case binding.ERR_INCLUDE:
- data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
+ data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
case validation.ErrGlobPattern:
- data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
+ data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
case validation.ErrRegexPattern:
- data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
+ data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername:
- data["ErrorMsg"] = trName + l.Tr("form.username_error")
+ data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap:
- data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
+ data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
@@ -146,7 +146,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
msg += errs[0].Message
if msg == "" {
- msg = l.Tr("form.unknown_error")
+ msg = l.TrString("form.unknown_error")
}
data["ErrorMsg"] = trName + ": " + msg
}
diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go
index 41f3aac27c..88da2049a4 100644
--- a/modules/web/middleware/flash.go
+++ b/modules/web/middleware/flash.go
@@ -3,7 +3,11 @@
package middleware
-import "net/url"
+import (
+ "fmt"
+ "html/template"
+ "net/url"
+)
// Flash represents a one time data transfer between two requests.
type Flash struct {
@@ -26,26 +30,36 @@ func (f *Flash) set(name, msg string, current ...bool) {
}
}
+func flashMsgStringOrHTML(msg any) string {
+ switch v := msg.(type) {
+ case string:
+ return v
+ case template.HTML:
+ return string(v)
+ }
+ panic(fmt.Sprintf("unknown type: %T", msg))
+}
+
// Error sets error message
-func (f *Flash) Error(msg string, current ...bool) {
- f.ErrorMsg = msg
- f.set("error", msg, current...)
+func (f *Flash) Error(msg any, current ...bool) {
+ f.ErrorMsg = flashMsgStringOrHTML(msg)
+ f.set("error", f.ErrorMsg, current...)
}
// Warning sets warning message
-func (f *Flash) Warning(msg string, current ...bool) {
- f.WarningMsg = msg
- f.set("warning", msg, current...)
+func (f *Flash) Warning(msg any, current ...bool) {
+ f.WarningMsg = flashMsgStringOrHTML(msg)
+ f.set("warning", f.WarningMsg, current...)
}
// Info sets info message
-func (f *Flash) Info(msg string, current ...bool) {
- f.InfoMsg = msg
- f.set("info", msg, current...)
+func (f *Flash) Info(msg any, current ...bool) {
+ f.InfoMsg = flashMsgStringOrHTML(msg)
+ f.set("info", f.InfoMsg, current...)
}
// Success sets success message
-func (f *Flash) Success(msg string, current ...bool) {
- f.SuccessMsg = msg
- f.set("success", msg, current...)
+func (f *Flash) Success(msg any, current ...bool) {
+ f.SuccessMsg = flashMsgStringOrHTML(msg)
+ f.set("success", f.SuccessMsg, current...)
}
diff --git a/options/license/AMD-newlib b/options/license/AMD-newlib
new file mode 100644
index 0000000000..1b2f1abd6f
--- /dev/null
+++ b/options/license/AMD-newlib
@@ -0,0 +1,11 @@
+Copyright 1989, 1990 Advanced Micro Devices, Inc.
+
+This software is the property of Advanced Micro Devices, Inc (AMD) which
+specifically grants the user the right to modify, use and distribute this
+software provided this notice is not removed or altered. All other rights
+are reserved by AMD.
+
+AMD MAKES NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THIS
+SOFTWARE. IN NO EVENT SHALL AMD BE LIABLE FOR INCIDENTAL OR CONSEQUENTIAL
+DAMAGES IN CONNECTION WITH OR ARISING FROM THE FURNISHING, PERFORMANCE, OR
+USE OF THIS SOFTWARE.
diff --git a/options/license/APL-1.0 b/options/license/APL-1.0
index 261f2d687c..0748f90cd9 100644
--- a/options/license/APL-1.0
+++ b/options/license/APL-1.0
@@ -210,21 +210,21 @@ PART 1: INITIAL CONTRIBUTOR AND DESIGNATED WEB SITE
The Initial Contributor is:
____________________________________________________
-
+
[Enter full name of Initial Contributor]
Address of Initial Contributor:
________________________________________________
-
+
________________________________________________
-
+
________________________________________________
-
+
[Enter address above]
The Designated Web Site is:
__________________________________________________
-
+
[Enter URL for Designated Web Site of Initial Contributor]
NOTE: The Initial Contributor is to complete this Part 1, along with Parts 2, 3, and 5, and, if applicable, Parts 4 and 6.
@@ -237,27 +237,27 @@ The date on which the Initial Work was first available under this License: _____
PART 3: GOVERNING JURISDICTION
-For the purposes of this License, the Governing Jurisdiction is _________________________________________________.
[Initial Contributor to Enter Governing Jurisdiction here]
+For the purposes of this License, the Governing Jurisdiction is _________________________________________________. [Initial Contributor to Enter Governing Jurisdiction here]
PART 4: THIRD PARTIES
For the purposes of this License, "Third Party" has the definition set forth below in the ONE paragraph selected by the Initial Contributor from paragraphs A, B, C, D and E when the Initial Work is distributed or otherwise made available by the Initial Contributor. To select one of the following paragraphs, the Initial Contributor must place an "X" or "x" in the selection box alongside the one respective paragraph selected.
SELECTION
-
+
BOX PARAGRAPH
-[ ] A. "THIRD PARTY" means any third party.
-
-
-[ ] B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
-
-
-[ ] C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
-
-
-[ ] D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
-
-
-[ ] E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
+[ ] A. "THIRD PARTY" means any third party.
+
+
+[ ] B. "THIRD PARTY" means any third party except for any of the following: (a) a wholly owned subsidiary of the Subsequent Contributor in question; (b) a legal entity (the "PARENT") that wholly owns the Subsequent Contributor in question; or (c) a wholly owned subsidiary of the wholly owned subsidiary in (a) or of the Parent in (b).
+
+
+[ ] C. "THIRD PARTY" means any third party except for any of the following: (a) any Person directly or indirectly owning a majority of the voting interest in the Subsequent Contributor or (b) any Person in which the Subsequent Contributor directly or indirectly owns a majority voting interest.
+
+
+[ ] D. "THIRD PARTY" means any third party except for any Person directly or indirectly controlled by the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
+
+
+[ ] E. "THIRD PARTY" means any third party except for any Person directly or indirectly controlling, controlled by, or under common control with the Subsequent Contributor. For purposes of this definition, "control" shall mean the power to direct or cause the direction of, the management and policies of such Person whether through the ownership of voting interests, by contract, or otherwise.
The default definition of "THIRD PARTY" is the definition set forth in paragraph A, if NONE OR MORE THAN ONE of paragraphs A, B, C, D or E in this Part 4 are selected by the Initial Contributor.
PART 5: NOTICE
@@ -271,8 +271,8 @@ PART 6: PATENT LICENSING TERMS
For the purposes of this License, paragraphs A, B, C, D and E of this Part 6 of Exhibit A are only incorporated and form part of the terms of the License if the Initial Contributor places an "X" or "x" in the selection box alongside the YES answer to the question immediately below.
Is this a Patents-Included License pursuant to Section 2.2 of the License?
-YES [ ]
-NO [ ]
+YES [ ]
+NO [ ]
By default, if YES is not selected by the Initial Contributor, the answer is NO.
diff --git a/options/license/Brian-Gladman-2-Clause b/options/license/Brian-Gladman-2-Clause
new file mode 100644
index 0000000000..7276f63e9e
--- /dev/null
+++ b/options/license/Brian-Gladman-2-Clause
@@ -0,0 +1,17 @@
+Copyright (C) 1998-2013, Brian Gladman, Worcester, UK. All
+ rights reserved.
+
+The redistribution and use of this software (with or without
+changes) is allowed without the payment of fees or royalties
+provided that:
+
+ source code distributions include the above copyright notice,
+ this list of conditions and the following disclaimer;
+
+ binary distributions include the above copyright notice, this
+ list of conditions and the following disclaimer in their
+ documentation.
+
+This software is provided 'as is' with no explicit or implied
+warranties in respect of its operation, including, but not limited
+to, correctness and fitness for purpose.
diff --git a/options/license/CMU-Mach-nodoc b/options/license/CMU-Mach-nodoc
new file mode 100644
index 0000000000..c81d74fee7
--- /dev/null
+++ b/options/license/CMU-Mach-nodoc
@@ -0,0 +1,11 @@
+Copyright (C) 2002 Naval Research Laboratory (NRL/CCS)
+
+Permission to use, copy, modify and distribute this software and
+its documentation is hereby granted, provided that both the
+copyright notice and this permission notice appear in all copies of
+the software, derivative works or modified versions, and any
+portions thereof.
+
+NRL ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS" CONDITION AND
+DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY DAMAGES WHATSOEVER
+RESULTING FROM THE USE OF THIS SOFTWARE.
diff --git a/options/license/GNOME-examples-exception b/options/license/GNOME-examples-exception
new file mode 100644
index 0000000000..0f0cd53b50
--- /dev/null
+++ b/options/license/GNOME-examples-exception
@@ -0,0 +1 @@
+As a special exception, the copyright holders give you permission to copy, modify, and distribute the example code contained in this document under the terms of your choosing, without restriction.
diff --git a/options/license/Gmsh-exception b/options/license/Gmsh-exception
new file mode 100644
index 0000000000..6d28f704e4
--- /dev/null
+++ b/options/license/Gmsh-exception
@@ -0,0 +1,16 @@
+The copyright holders of Gmsh give you permission to combine Gmsh
+ with code included in the standard release of Netgen (from Joachim
+ Sch"oberl), METIS (from George Karypis at the University of
+ Minnesota), OpenCASCADE (from Open CASCADE S.A.S) and ParaView
+ (from Kitware, Inc.) under their respective licenses. You may copy
+ and distribute such a system following the terms of the GNU GPL for
+ Gmsh and the licenses of the other code concerned, provided that
+ you include the source code of that other code when and as the GNU
+ GPL requires distribution of source code.
+
+ Note that people who make modified versions of Gmsh are not
+ obligated to grant this special exception for their modified
+ versions; it is their choice whether to do so. The GNU General
+ Public License gives permission to release a modified version
+ without this exception; this exception also makes it possible to
+ release a modified version which carries forward this exception.
diff --git a/options/license/HPND-Fenneberg-Livingston b/options/license/HPND-Fenneberg-Livingston
new file mode 100644
index 0000000000..aaf524f3aa
--- /dev/null
+++ b/options/license/HPND-Fenneberg-Livingston
@@ -0,0 +1,13 @@
+Copyright (C) 1995,1996,1997,1998 Lars Fenneberg
+
+Permission to use, copy, modify, and distribute this software for any
+purpose and without fee is hereby granted, provided that this copyright and
+permission notice appear on all copies and supporting documentation, the
+name of Lars Fenneberg not be used in advertising or publicity pertaining to
+distribution of the program without specific prior permission, and notice be
+given in supporting documentation that copying and distribution is by
+permission of Lars Fenneberg.
+
+Lars Fenneberg makes no representations about the suitability of this
+software for any purpose. It is provided "as is" without express or implied
+warranty.
diff --git a/options/license/HPND-INRIA-IMAG b/options/license/HPND-INRIA-IMAG
new file mode 100644
index 0000000000..87d09d92cb
--- /dev/null
+++ b/options/license/HPND-INRIA-IMAG
@@ -0,0 +1,9 @@
+This software is available with usual "research" terms with
+the aim of retain credits of the software. Permission to use,
+copy, modify and distribute this software for any purpose and
+without fee is hereby granted, provided that the above copyright
+notice and this permission notice appear in all copies, and
+the name of INRIA, IMAG, or any contributor not be used in
+advertising or publicity pertaining to this material without
+the prior explicit permission. The software is provided "as
+is" without any warranties, support or liabilities of any kind.
diff --git a/options/license/IBM-pibs b/options/license/IBM-pibs
index 49454b8b1e..ee9c7be36d 100644
--- a/options/license/IBM-pibs
+++ b/options/license/IBM-pibs
@@ -4,5 +4,5 @@ Any user of this software should understand that IBM cannot provide technical su
Any person who transfers this source code or any derivative work must include the IBM copyright notice, this paragraph, and the preceding two paragraphs in the transferred software.
-COPYRIGHT I B M CORPORATION 2002
-LICENSED MATERIAL - PROGRAM PROPERTY OF I B M
+COPYRIGHT I B M CORPORATION 2002
+LICENSED MATERIAL - PROGRAM PROPERTY OF I B M
diff --git a/options/license/MIT-Khronos-old b/options/license/MIT-Khronos-old
new file mode 100644
index 0000000000..430863bc98
--- /dev/null
+++ b/options/license/MIT-Khronos-old
@@ -0,0 +1,23 @@
+Copyright (c) 2014-2020 The Khronos Group Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and/or associated documentation files (the "Materials"),
+to deal in the Materials without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Materials, and to permit persons to whom the
+Materials are furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Materials.
+
+MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS
+STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND
+HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/
+
+THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS
+IN THE MATERIALS.
diff --git a/options/license/Mackerras-3-Clause b/options/license/Mackerras-3-Clause
new file mode 100644
index 0000000000..6467f0c98e
--- /dev/null
+++ b/options/license/Mackerras-3-Clause
@@ -0,0 +1,25 @@
+Copyright (c) 1995 Eric Rosenquist. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ 3. The name(s) of the authors of this software must not be used to
+ endorse or promote products derived from this software without
+ prior written permission.
+
+ THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+ THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+ SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/options/license/Mackerras-3-Clause-acknowledgment b/options/license/Mackerras-3-Clause-acknowledgment
new file mode 100644
index 0000000000..5f0187add7
--- /dev/null
+++ b/options/license/Mackerras-3-Clause-acknowledgment
@@ -0,0 +1,25 @@
+Copyright (c) 1993-2002 Paul Mackerras. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. The name(s) of the authors of this software must not be used to
+ endorse or promote products derived from this software without
+ prior written permission.
+
+3. Redistributions of any form whatsoever must retain the following
+ acknowledgment:
+ "This product includes software developed by Paul Mackerras
+ ".
+
+THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/options/license/NCGL-UK-2.0 b/options/license/NCGL-UK-2.0
index 31fbad6f83..15c4f63c22 100644
--- a/options/license/NCGL-UK-2.0
+++ b/options/license/NCGL-UK-2.0
@@ -12,15 +12,15 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
You are free to:
- copy, publish, distribute and transmit the Information;
+ copy, publish, distribute and transmit the Information;
adapt the Information;
exploit the Information for Non-Commercial purposes for example, by combining it with other information in your own product or application.
You are not permitted to:
- exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
+ exercise any of the rights granted to you by this licence in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation.
You must, where you do any of the above:
- acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
+ acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
If the Information Provider does not provide a specific attribution statement, you must use the following:
Contains information licensed under the Non-Commercial Government Licence v2.0.
diff --git a/options/license/NPL-1.1 b/options/license/NPL-1.1
index 62c5296400..0d5457ff04 100644
--- a/options/license/NPL-1.1
+++ b/options/license/NPL-1.1
@@ -2,7 +2,7 @@ Netscape Public LIcense version 1.1
AMENDMENTS
-The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License. Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
+The Netscape Public License Version 1.1 ("NPL") consists of the Mozilla Public License Version 1.1 with the following Amendments, including Exhibit A-Netscape Public License. Files identified with "Exhibit A-Netscape Public License" are governed by the Netscape Public License Version 1.1.
Additional Terms applicable to the Netscape Public License.
@@ -28,7 +28,7 @@ Additional Terms applicable to the Netscape Public License.
Notwithstanding the limitations of Section 11 above, the provisions regarding litigation in Section 11(a), (b) and (c) of the License shall apply to all disputes relating to this License.
EXHIBIT A-Netscape Public License.
-
+
"The contents of this file are subject to the Netscape Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/NPL/
Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
@@ -37,8 +37,8 @@ The Original Code is Mozilla Communicator client code, released March 31, 1998.
The Initial Developer of the Original Code is Netscape Communications Corporation. Portions created by Netscape are Copyright (C) 1998-1999 Netscape Communications Corporation. All Rights Reserved.
Contributor(s): ______________________________________.
-
-Alternatively, the contents of this file may be used under the terms of the _____ license (the "[___] License"), in which case the provisions of [______] License are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the [___] License. If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
+
+Alternatively, the contents of this file may be used under the terms of the _____ license (the "[___] License"), in which case the provisions of [______] License are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the [____] License and not to allow others to use your version of this file under the NPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the [___] License. If you do not delete the provisions above, a recipient may use your version of this file under either the NPL or the [___] License."
Mozilla Public License Version 1.1
diff --git a/options/license/OAR b/options/license/OAR
new file mode 100644
index 0000000000..ca5c4b9617
--- /dev/null
+++ b/options/license/OAR
@@ -0,0 +1,12 @@
+COPYRIGHT (c) 1989-2013, 2015.
+On-Line Applications Research Corporation (OAR).
+
+Permission to use, copy, modify, and distribute this software for any
+purpose without fee is hereby granted, provided that this entire notice
+is included in all copies of any software which is or includes a copy
+or modification of this software.
+
+THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION
+OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS
+SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.
diff --git a/options/license/OCCT-PL b/options/license/OCCT-PL
index 85df3c73c5..9b6fccc1c9 100644
--- a/options/license/OCCT-PL
+++ b/options/license/OCCT-PL
@@ -6,7 +6,7 @@ OPEN CASCADE releases and makes publicly available the source code of the softwa
It is not the purpose of this license to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this license has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
Please read this license carefully and completely before downloading this software. By downloading, using, modifying, distributing and sublicensing this software, you indicate your acceptance to be bound by the terms and conditions of this license. If you do not want to accept or cannot accept for any reasons the terms and conditions of this license, please do not download or use in any manner this software.
-
+
1. Definitions
Unless there is something in the subject matter or in the context inconsistent therewith, the capitalized terms used in this License shall have the following meaning.
@@ -26,13 +26,13 @@ Unless there is something in the subject matter or in the context inconsistent t
"Software": means the Original Code, the Modifications, the combination of Original Code and any Modifications or any respective portions thereof.
"You" or "Your": means an individual or a legal entity exercising rights under this License
-
+
2. Acceptance of license
By using, reproducing, modifying, distributing or sublicensing the Software or any portion thereof, You expressly indicate Your acceptance of the terms and conditions of this License and undertake to act in accordance with all the provisions of this License applicable to You.
-
+
3. Scope and purpose
This License applies to the Software and You may not use, reproduce, modify, distribute, sublicense or circulate the Software, or any portion thereof, except as expressly provided under this License. Any attempt to otherwise use, reproduce, modify, distribute or sublicense the Software is void and will automatically terminate Your rights under this License.
-
+
4. Contributor license
Subject to the terms and conditions of this License, the Initial Developer and each of the Contributors hereby grant You a world-wide, royalty-free, irrevocable and non-exclusive license under the Applicable Intellectual Property Rights they own or control, to use, reproduce, modify, distribute and sublicense the Software provided that:
diff --git a/options/license/OGL-UK-1.0 b/options/license/OGL-UK-1.0
index a761c9916f..867c0e353b 100644
--- a/options/license/OGL-UK-1.0
+++ b/options/license/OGL-UK-1.0
@@ -10,20 +10,20 @@ The Licensor grants you a worldwide, royalty-free, perpetual, non-exclusive lice
This licence does not affect your freedom under fair dealing or fair use or any other copyright or database right exceptions and limitations.
You are free to:
- copy, publish, distribute and transmit the Information;
+ copy, publish, distribute and transmit the Information;
adapt the Information;
exploit the Information commercially for example, by combining it with other Information, or by including it in your own product or application.
You must, where you do any of the above:
- acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
- If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following:
Contains public sector information licensed under the Open Government Licence v1.0.
+ acknowledge the source of the Information by including any attribution statement specified by the Information Provider(s) and, where possible, provide a link to this licence;
+ If the Information Provider does not provide a specific attribution statement, or if you are using Information from several Information Providers and multiple attributions are not practical in your product or application, you may consider using the following: Contains public sector information licensed under the Open Government Licence v1.0.
ensure that you do not use the Information in a way that suggests any official status or that the Information Provider endorses you or your use of the Information;
ensure that you do not mislead others or misrepresent the Information or its source;
ensure that your use of the Information does not breach the Data Protection Act 1998 or the Privacy and Electronic Communications (EC Directive) Regulations 2003.
These are important conditions of this licence and if you fail to comply with them the rights granted to you under this licence, or any similar licence granted by the Licensor, will end automatically.
- Exemptions
+ Exemptions
This licence does not cover the use of:
- personal data in the Information;
@@ -48,22 +48,22 @@ Definitions
In this licence, the terms below have the following meanings:
-‘Information’
means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
+‘Information’ means information protected by copyright or by database right (for example, literary and artistic works, content, data and source code) offered for use under the terms of this licence.
-‘Information Provider’
means the person or organisation providing the Information under this licence.
+‘Information Provider’ means the person or organisation providing the Information under this licence.
-‘Licensor’
means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
+‘Licensor’ means any Information Provider which has the authority to offer Information under the terms of this licence or the Controller of Her Majesty’s Stationery Office, who has the authority to offer Information subject to Crown copyright and Crown database rights and Information subject to copyright and database right that has been assigned to or acquired by the Crown, under the terms of this licence.
-‘Use’
as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
+‘Use’ as a verb, means doing any act which is restricted by copyright or database right, whether in the original medium or in any other medium, and includes without limitation distributing, copying, adapting, modifying as may be technically necessary to use it in a different mode or format.
-‘You’
means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
+‘You’ means the natural or legal person, or body of persons corporate or incorporate, acquiring rights under this licence.
About the Open Government Licence
The Controller of Her Majesty’s Stationery Office (HMSO) has developed this licence as a tool to enable Information Providers in the public sector to license the use and re-use of their Information under a common open licence. The Controller invites public sector bodies owning their own copyright and database rights to permit the use of their Information under this licence.
-The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
+The Controller of HMSO has authority to license Information subject to copyright and database right owned by the Crown. The extent of the Controller’s offer to license this Information under the terms of this licence is set out in the UK Government Licensing Framework.
This is version 1.0 of the Open Government Licence. The Controller of HMSO may, from time to time, issue new versions of the Open Government Licence. However, you may continue to use Information licensed under this version should you wish to do so.
These terms have been aligned to be interoperable with any Creative Commons Attribution Licence, which covers copyright, and Open Data Commons Attribution License, which covers database rights and applicable copyrights.
-Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
+Further context, best practice and guidance can be found in the UK Government Licensing Framework section on The National Archives website.
diff --git a/options/license/OSET-PL-2.1 b/options/license/OSET-PL-2.1
index 15f0c7758c..e0ed2e1398 100644
--- a/options/license/OSET-PL-2.1
+++ b/options/license/OSET-PL-2.1
@@ -100,7 +100,8 @@ If it is impossible for You to comply with any of the terms of this License with
5.1 Failure to Comply
The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60-days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30-days after Your receipt of the notice.
- 5.2 Patent Infringement Claims
If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
+ 5.2 Patent Infringement Claims
+ If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
5.3 Additional Compliance Terms
Notwithstanding the foregoing in this Section 5, for purposes of this Section, if You breach Section 3.1 (Distribution of Source Form), Section 3.2 (Distribution of Executable Form), Section 3.3 (Distribution of a Larger Work), or Section 3.4 (Notices), then becoming compliant as described in Section 5.1 must also include, no later than 30 days after receipt by You of notice of such violation by a Contributor, making the Covered Software available in Source Code Form as required by this License on a publicly available computer network for a period of no less than three (3) years.
diff --git a/options/license/OpenVision b/options/license/OpenVision
new file mode 100644
index 0000000000..983505389e
--- /dev/null
+++ b/options/license/OpenVision
@@ -0,0 +1,33 @@
+Copyright, OpenVision Technologies, Inc., 1993-1996, All Rights
+Reserved
+
+WARNING: Retrieving the OpenVision Kerberos Administration system
+source code, as described below, indicates your acceptance of the
+following terms. If you do not agree to the following terms, do
+not retrieve the OpenVision Kerberos administration system.
+
+You may freely use and distribute the Source Code and Object Code
+compiled from it, with or without modification, but this Source
+Code is provided to you "AS IS" EXCLUSIVE OF ANY WARRANTY,
+INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY OR
+FITNESS FOR A PARTICULAR PURPOSE, OR ANY OTHER WARRANTY, WHETHER
+EXPRESS OR IMPLIED. IN NO EVENT WILL OPENVISION HAVE ANY LIABILITY
+FOR ANY LOST PROFITS, LOSS OF DATA OR COSTS OF PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES, OR FOR ANY SPECIAL, INDIRECT, OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT, INCLUDING,
+WITHOUT LIMITATION, THOSE RESULTING FROM THE USE OF THE SOURCE
+CODE, OR THE FAILURE OF THE SOURCE CODE TO PERFORM, OR FOR ANY
+OTHER REASON.
+
+OpenVision retains all copyrights in the donated Source Code.
+OpenVision also retains copyright to derivative works of the Source
+Code, whether created by OpenVision or by a third party. The
+OpenVision copyright notice must be preserved if derivative works
+are made based on the donated Source Code.
+
+OpenVision Technologies, Inc. has donated this Kerberos
+Administration system to MIT for inclusion in the standard Kerberos
+5 distribution. This donation underscores our commitment to
+continuing Kerberos technology development and our gratitude for
+the valuable work which has been performed by MIT and the Kerberos
+community.
diff --git a/options/license/SHL-2.0 b/options/license/SHL-2.0
index e522a396fe..9218b47a72 100644
--- a/options/license/SHL-2.0
+++ b/options/license/SHL-2.0
@@ -1,22 +1,22 @@
# Solderpad Hardware Licence Version 2.0
-This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
+This licence (the “Licence”) operates as a wraparound licence to the Apache License Version 2.0 (the “Apache License”) and grants to You the rights, and imposes the obligations, set out in the Apache License (which can be found here: http://apache.org/licenses/LICENSE-2.0), with the following extensions. It must be read in conjunction with the Apache License. Section 1 below modifies definitions in the Apache License, and section 2 below replaces sections 2 of the Apache License. You may, at your option, choose to treat any Work released under this License as released under the Apache License (thus ignoring all sections written below entirely). Words in italics indicate changes rom the Apache License, but are indicative and not to be taken into account in interpretation.
1. The definitions set out in the Apache License are modified as follows:
-Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
+Copyright any reference to ‘copyright’ (whether capitalised or not) includes ‘Rights’ (as defined below).
-Contribution also includes any design, as well as any work of authorship.
+Contribution also includes any design, as well as any work of authorship.
-Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
+Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the interfaces of the Work and Derivative Works thereof.
-Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
+Object form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
-Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
+Rights means copyright and any similar right including design right (whether registered or unregistered), semiconductor topography (mask) rights and database rights (but excluding Patents and Trademarks).
-Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
-Work also includes a design or work of authorship, whether in Source form or other Object form.
+Source form shall mean the preferred form for making modifications, including but not limited to source code, net lists, board layouts, CAD files, documentation source, and configuration files.
+Work also includes a design or work of authorship, whether in Source form or other Object form.
2. Grant of Licence
-2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
+2.1 Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license under the Rights to reproduce, prepare Derivative Works of, make, adapt, repair, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form and do anything in relation to the Work as if the Rights did not exist.
diff --git a/options/license/SHL-2.1 b/options/license/SHL-2.1
index 4815a9e5ed..c9ae53741f 100644
--- a/options/license/SHL-2.1
+++ b/options/license/SHL-2.1
@@ -19,7 +19,7 @@ The following definitions shall replace the corresponding definitions in the Apa
"License" shall mean this Solderpad Hardware License version 2.1, being the terms and conditions for use, manufacture, instantiation, adaptation, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the Rights owner or entity authorized by the Rights owner that is granting the License.
-
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship or design. For the purposes of this License, Derivative Works shall not include works that remain reversibly separable from, or merely link (or bind by name) or physically connect to or interoperate with the Work and Derivative Works thereof.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form or the application of a Source form to physical material, including but not limited to compiled object code, generated documentation, the instantiation of a hardware design or physical object or material and conversions to other media types, including intermediate forms such as bytecodes, FPGA bitstreams, moulds, artwork and semiconductor topographies (mask works).
diff --git a/options/license/SISSL b/options/license/SISSL
index 7d6ad9d66c..af38d02d92 100644
--- a/options/license/SISSL
+++ b/options/license/SISSL
@@ -36,13 +36,13 @@ Sun Industry Standards Source License - Version 1.1
2.0 SOURCE CODE LICENSE
- 2.1 The Initial Developer Grant The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:
+ 2.1 The Initial Developer Grant The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:
(a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, and/or as part of a Larger Work; and
(b) under Patents Claims infringed by the making, using or selling of Original Code, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Code (or portions thereof).
(c) the licenses granted in this Section 2.1(a) and (b) are effective on the date Initial Developer first distributes Original Code under the terms of this License.
- (d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications.
+ (d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices, including but not limited to Modifications.
3.0 DISTRIBUTION OBLIGATIONS
@@ -92,14 +92,14 @@ This License represents the complete agreement concerning subject matter hereof.
EXHIBIT A - Sun Standards License
-"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
+"The contents of this file are subject to the Sun Standards License Version 1.1 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at _______________________________.
-Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
+Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
express or implied. See the License for the specific language governing rights and limitations under the License.
The Original Code is ______________________________________.
-The Initial Developer of the Original Code is:
+The Initial Developer of the Original Code is:
Sun Microsystems, Inc..
Portions created by: _______________________________________
diff --git a/options/license/Sun-PPP b/options/license/Sun-PPP
new file mode 100644
index 0000000000..5f94a13437
--- /dev/null
+++ b/options/license/Sun-PPP
@@ -0,0 +1,13 @@
+Copyright (c) 2001 by Sun Microsystems, Inc.
+All rights reserved.
+
+Non-exclusive rights to redistribute, modify, translate, and use
+this software in source and binary forms, in whole or in part, is
+hereby granted, provided that the above copyright notice is
+duplicated in any source form, and that neither the name of the
+copyright holder nor the author is used to endorse or promote
+products derived from this software.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
diff --git a/options/license/UMich-Merit b/options/license/UMich-Merit
new file mode 100644
index 0000000000..93e304b90e
--- /dev/null
+++ b/options/license/UMich-Merit
@@ -0,0 +1,19 @@
+[C] The Regents of the University of Michigan and Merit Network, Inc. 1992,
+1993, 1994, 1995 All Rights Reserved
+
+Permission to use, copy, modify, and distribute this software and its
+documentation for any purpose and without fee is hereby granted, provided
+that the above copyright notice and this permission notice appear in all
+copies of the software and derivative works or modified versions thereof,
+and that both the copyright notice and this permission and disclaimer
+notice appear in supporting documentation.
+
+THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE REGENTS OF THE
+UNIVERSITY OF MICHIGAN AND MERIT NETWORK, INC. DO NOT WARRANT THAT THE
+FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET LICENSEE'S REQUIREMENTS OR
+THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. The Regents of the
+University of Michigan and Merit Network, Inc. shall not be liable for any
+special, indirect, incidental or consequential damages with respect to any
+claim by Licensee or any third party arising from use of the software.
diff --git a/options/license/W3C-19980720 b/options/license/W3C-19980720
index a8554039ef..134879044d 100644
--- a/options/license/W3C-19980720
+++ b/options/license/W3C-19980720
@@ -4,7 +4,7 @@ Copyright (c) 1994-2002 World Wide Web Consortium, (Massachusetts Institute of T
This W3C work (including software, documents, or other related items) is being provided by the copyright holders under the following license. By obtaining, using and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions:
-Permission to use, copy, modify, and distribute this software and its documentation, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
+Permission to use, copy, modify, and distribute this software and its documentation, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the software and documentation or portions thereof, including modifications, that you make:
1. The full text of this NOTICE in a location viewable to users of the redistributed or derivative work.
diff --git a/options/license/bcrypt-Solar-Designer b/options/license/bcrypt-Solar-Designer
new file mode 100644
index 0000000000..8cb05017fc
--- /dev/null
+++ b/options/license/bcrypt-Solar-Designer
@@ -0,0 +1,11 @@
+Written by Solar Designer in 1998-2014.
+No copyright is claimed, and the software is hereby placed in the public
+domain. In case this attempt to disclaim copyright and place the software
+in the public domain is deemed null and void, then the software is
+Copyright (c) 1998-2014 Solar Designer and it is hereby released to the
+general public under the following terms:
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted.
+
+There's ABSOLUTELY NO WARRANTY, express or implied.
diff --git a/options/license/gtkbook b/options/license/gtkbook
new file mode 100644
index 0000000000..91215e80d6
--- /dev/null
+++ b/options/license/gtkbook
@@ -0,0 +1,6 @@
+Copyright 2005 Syd Logan, All Rights Reserved
+
+This code is distributed without warranty. You are free to use
+this code for any purpose, however, if this code is republished or
+redistributed in its original form, as hardcopy or electronically,
+then you must include this copyright notice along with the code.
diff --git a/options/license/softSurfer b/options/license/softSurfer
new file mode 100644
index 0000000000..1bbc88c34c
--- /dev/null
+++ b/options/license/softSurfer
@@ -0,0 +1,6 @@
+Copyright 2001, softSurfer (www.softsurfer.com)
+This code may be freely used and modified for any purpose
+providing that this copyright notice is included with it.
+SoftSurfer makes no warranty for this code, and cannot be held
+liable for any real or imagined damage resulting from its use.
+Users of this code must verify correctness for their application.
diff --git a/options/license/threeparttable b/options/license/threeparttable
new file mode 100644
index 0000000000..498b728226
--- /dev/null
+++ b/options/license/threeparttable
@@ -0,0 +1,3 @@
+This file may be distributed, modified, and used in other works with just
+one restriction: modified versions must clearly indicate the modification
+(a name change, or a displayed message, or ?).
diff --git a/options/license/xzoom b/options/license/xzoom
new file mode 100644
index 0000000000..f312dedbc2
--- /dev/null
+++ b/options/license/xzoom
@@ -0,0 +1,12 @@
+Copyright Itai Nahshon 1995, 1996.
+This program is distributed with no warranty.
+
+Source files for this program may be distributed freely.
+Modifications to this file are okay as long as:
+ a. This copyright notice and comment are preserved and
+ left at the top of the file.
+ b. The man page is fixed to reflect the change.
+ c. The author of this change adds his name and change
+ description to the list of changes below.
+Executable files may be distributed with sources, or with
+exact location where the source code can be obtained.
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index e9428ebcd4..82a8fe5d45 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -4,6 +4,7 @@ explore=Procházet
help=Nápověda
logo=Logo
sign_in=Přihlásit se
+sign_in_with_provider=Přihlásit se pomocí %s
sign_in_or=nebo
sign_out=Odhlásit se
sign_up=Registrovat se
@@ -16,6 +17,7 @@ template=Šablona
language=Jazyk
notifications=Oznámení
active_stopwatch=Aktivní sledování času
+tracked_time_summary=Shrnutí sledovaného času na základě filtrů v seznamu úkolů
create_new=Vytvořit…
user_profile_and_more=Profily a nastavení…
signed_in_as=Přihlášen jako
@@ -23,6 +25,7 @@ enable_javascript=Tato stránka vyžaduje JavaScript.
toc=Obsah
licenses=Licence
return_to_gitea=Vrátit se do Gitea
+more_items=Více položek
username=Uživatelské jméno
email=E-mailová adresa
@@ -73,12 +76,13 @@ collaborative=Spolupráce
forks=Rozštěpení
activities=Aktivity
-pull_requests=Požadavky na natažení
+pull_requests=Pull requesty
issues=Úkoly
milestones=Milníky
ok=OK
cancel=Zrušit
+retry=Znovu
rerun=Znovu spustit
rerun_all=Znovu spustit všechny úlohy
save=Uložit
@@ -86,14 +90,17 @@ add=Přidat
add_all=Přidat vše
remove=Odstranit
remove_all=Odstranit vše
-remove_label_str=`Odstranit položku "%s"`
+remove_label_str=Odstranit položku „%s“
edit=Upravit
+view=Zobrazit
enabled=Povolený
disabled=Zakázané
+locked=Uzamčeno
copy=Kopírovat
copy_url=Kopírovat URL
+copy_hash=Kopírovat hash
copy_content=Kopírovat obsah
copy_branch=Kopírovat jméno větve
copy_success=Zkopírováno!
@@ -106,6 +113,8 @@ loading=Načítá se…
error=Chyba
error404=Stránka, kterou se snažíte zobrazit, buď neexistuje, nebo nemáte oprávnění ji zobrazit.
+go_back=Zpět
+invalid_data=Neplatná data: %v
never=Nikdy
unknown=Neznámý
@@ -116,6 +125,7 @@ pin=Připnout
unpin=Odepnout
artifacts=Artefakty
+confirm_delete_artifact=Jste si jisti, že chcete odstranit artefakt „%s“?
archived=Archivováno
@@ -127,11 +137,50 @@ concept_user_organization=Organizace
show_timestamps=Zobrazit časové značky
show_log_seconds=Zobrazit sekundy
show_full_screen=Zobrazit celou obrazovku
+download_logs=Stáhnout logy
+confirm_delete_selected=Potvrdit odstranění všech vybraných položek?
name=Název
value=Hodnota
+filter=Filtr
+filter.clear=Vymazat filtr
+filter.is_archived=Archivováno
+filter.not_archived=Nearchivované
+filter.is_fork=Rozštěpený
+filter.not_fork=Není rozštěpený
+filter.is_mirror=Zrcadlen
+filter.not_mirror=Není zrcadleno
+filter.is_template=Šablona
+filter.not_template=Není šablona
+filter.public=Veřejná
+filter.private=Soukromý
+
+no_results_found=Nebyly nalezeny žádné výsledky.
+
+[search]
+search=Hledat...
+type_tooltip=Druh vyhledávání
+fuzzy=Fuzzy
+fuzzy_tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
+match=Shoda
+match_tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
+repo_kind=Hledat repozitáře...
+user_kind=Hledat uživatele...
+org_kind=Hledat organizace...
+team_kind=Hledat týmy...
+code_kind=Hledat kód...
+code_search_unavailable=Vyhledávání kódu není momentálně dostupné. Obraťte se na správce webu.
+code_search_by_git_grep=Aktuální výsledky vyhledávání kódu jsou poskytovány pomocí „git grep“. Pokud správce webu povolí index repozitáře, mohou být výsledky lepší.
+package_kind=Hledat balíčky...
+project_kind=Hledat projekty...
+branch_kind=Hledat větve...
+commit_kind=Hledat commity...
+runner_kind=Hledat runnery...
+no_results=Nebyly nalezeny žádné odpovídající výsledky.
+keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu.
+
[aria]
navbar=Navigační lišta
footer=Patička
@@ -153,7 +202,7 @@ buttons.code.tooltip=Přidat kód
buttons.link.tooltip=Přidat odkaz
buttons.list.unordered.tooltip=Přidat seznam odrážek
buttons.list.ordered.tooltip=Přidat číslovaný seznam
-buttons.list.task.tooltip=Přidat seznam úkolů
+buttons.list.task.tooltip=Přidat seznam úloh
buttons.mention.tooltip=Uveďte uživatele nebo tým
buttons.ref.tooltip=Odkaz na issue nebo pull request
buttons.switch_to_legacy.tooltip=Místo toho použít starší editor
@@ -166,6 +215,7 @@ string.desc=Z – A
[error]
occurred=Došlo k chybě
+report_message=Pokud jste si jisti, že se jedná o chybu Gitea, prosím vyhledejte problém na GitHub a v případě potřeby založte nový problém.
missing_csrf=Špatný požadavek: Neexistuje CSRF token
invalid_csrf=Špatný požadavek: Neplatný CSRF token
not_found=Cíl nebyl nalezen.
@@ -174,6 +224,7 @@ network_error=Chyba sítě
[startpage]
app_desc=Snadno přístupný vlastní Git
install=Jednoduchá na instalaci
+install_desc=Jednoduše spusťte jako binární program pro vaši platformu, nasaďte jej pomocí Docker, nebo jej stáhněte jako balíček.
platform=Multiplatformní
platform_desc=Gitea běží všude, kde Go může kompilovat: Windows, macOS, Linux, ARM, atd. Vyberte si ten, který milujete!
lightweight=Lehká
@@ -218,6 +269,7 @@ repo_path_helper=Všechny vzdálené repozitáře Gitu budou uloženy do tohoto
lfs_path=Kořenový adresář Git LFS
lfs_path_helper=V tomto adresáři budou uloženy soubory, které jsou sledovány Git LFS. Pokud ponecháte prázdné, LFS zakážete.
run_user=Spustit jako uživatel
+run_user_helper=Zadejte uživatelské jméno, pod kterým Gitea běží v operačním systému. Pozor: tento uživatel musí mít přístup ke kořenovému adresáři repozitářů.
domain=Doména serveru
domain_helper=Adresa domény, nebo hostitele serveru.
ssh_port=Port SSH serveru
@@ -234,6 +286,7 @@ email_title=Nastavení e-mailu
smtp_addr=Server SMTP
smtp_port=Port SMTP
smtp_from=Odeslat e-mail jako
+smtp_from_invalid=Adresa "Odeslat e-mail jako" je neplatná
smtp_from_helper=E-mailová adresa, kterou bude Gitea používat. Zadejte běžnou e-mailovou adresu, nebo použijte formát "Jméno".
mailer_user=Uživatelské jméno SMTP
mailer_password=Heslo pro SMTP
@@ -267,7 +320,7 @@ install_btn_confirm=Nainstalovat Gitea
test_git_failed=Chyba při testu příkazu 'git': %v
sqlite3_not_available=Tato verze Gitea nepodporuje SQLite3. Stáhněte si oficiální binární verzi od %s (nikoli verzi „gobuild“).
invalid_db_setting=Nastavení databáze je neplatné: %v
-invalid_db_table=Databázová tabulka "%s" je neplatná: %v
+invalid_db_table=Databázová tabulka „%s“ je neplatná: %v
invalid_repo_path=Kořenový adresář repozitářů není správný: %v
invalid_app_data_path=Cesta k datům aplikace je neplatná: %v
run_user_not_match=`"Run as" uživatelské jméno není aktuální uživatelské jméno: %s -> %s`
@@ -289,8 +342,11 @@ invalid_password_algorithm=Neplatný algoritmus hash hesla
password_algorithm_helper=Nastavte algoritmus hashování hesla. Algoritmy mají odlišné požadavky a sílu. Algoritmus argon2 je poměrně bezpečný, ale používá spoustu paměti a může být nevhodný pro malé systémy.
enable_update_checker=Povolit kontrolu aktualizací
enable_update_checker_helper=Kontroluje vydání nových verzí pravidelně připojením ke gitea.io.
+env_config_keys=Konfigurace prostředí
+env_config_keys_prompt=Následující proměnné prostředí budou také použity pro váš konfigurační soubor:
[home]
+nav_menu=Navigační menu
uname_holder=Uživatelské jméno nebo e-mailová adresa
password_holder=Heslo
switch_dashboard_context=Přepnout kontext přehledu
@@ -300,7 +356,6 @@ collaborative_repos=Společné repozitáře
my_orgs=Mé organizace
my_mirrors=Má zrcadla
view_home=Zobrazit %s
-search_repos=Nalézt repozitář…
filter=Ostatní filtry
filter_by_team_repositories=Filtrovat podle repozitářů týmu
feed_of=Kanál z „%s“
@@ -321,20 +376,8 @@ issues.in_your_repos=Ve vašich repozitářích
repos=Repozitáře
users=Uživatelé
organizations=Organizace
-search=Vyhledat
go_to=Přejít na
code=Kód
-search.type.tooltip=Druh vyhledávání
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
-search.match=Shoda
-search.match.tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu
-code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu.
-repo_no_results=Nebyly nalezeny žádné odpovídající repozitáře.
-user_no_results=Nebyly nalezeni žádní odpovídající uživatelé.
-org_no_results=Nebyly nalezeny žádné odpovídající organizace.
-code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu.
-code_search_results=`Výsledky hledání pro "%s"`
code_last_indexed_at=Naposledy indexováno %s
relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty.
relevant_repositories=Zobrazují se pouze relevantní repositáře, zobrazit nefiltrované výsledky.
@@ -347,25 +390,31 @@ disable_register_prompt=Registrace jsou vypnuty. Prosíme, kontaktujte správce
disable_register_mail=E-mailové potvrzení o registraci je zakázané.
manual_activation_only=Pro dokončení aktivace kontaktujte správce webu.
remember_me=Pamatovat si toto zařízení
+remember_me.compromised=Přihlašovací token již není platný, což může znamenat napadení účtu. Zkontrolujte prosím svůj účet pro neobvyklé aktivity.
forgot_password_title=Zapomenuté heslo
forgot_password=Zapomenuté heslo?
sign_up_now=Potřebujete účet? Zaregistrujte se.
-confirmation_mail_sent_prompt=Na adresu %s byl zaslán nový potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces registrace.
+sign_up_successful=Účet byl úspěšně vytvořen. Vítejte!
+confirmation_mail_sent_prompt_ex=Nový potvrzovací e-mail byl odeslán na %s. Zkontrolujte prosím svou doručenou poštu během následujících %s a dokončete proces registrace. Pokud je Vaše registrační e-mailová adresa nesprávná, můžete se znovu přihlásit a změnit ji.
must_change_password=Aktualizujte své heslo
allow_password_change=Vyžádat od uživatele změnu hesla (doporučeno)
reset_password_mail_sent_prompt=Na adresu %s byl zaslán potvrzovací e-mail. Zkontrolujte prosím vaši doručenou poštu během následujících %s, abyste dokončili proces obnovení účtu.
active_your_account=Aktivujte si váš účet
account_activated=Účet byl aktivován
prohibit_login=Přihlášení zakázáno
+prohibit_login_desc=Vašemu účtu je zakázáno se přihlásit, kontaktujte prosím správce webu.
resent_limit_prompt=Omlouváme se, ale před chvílí jste požádal o zaslání aktivačního e-mailu. Počkejte prosím 3 minuty a pak to zkuste znovu.
has_unconfirmed_mail=Zdravím, %s, máte nepotvrzenou e-mailovou adresu (%s). Pokud jste nedostali e-mail pro potvrzení nebo potřebujete zaslat nový, klikněte prosím na tlačítku níže.
+change_unconfirmed_mail_address=Pokud je Vaše registrační e-mailová adresa nesprávná, můžete ji zde změnit a znovu odeslat nový potvrzovací e-mail.
resend_mail=Klikněte zde pro odeslání aktivačního e-mailu
email_not_associate=Tato e-mailová adresa není spojena s žádným účtem.
send_reset_mail=Zaslat e-mail pro obnovení účtu
reset_password=Obnovení účtu
invalid_code=Tento potvrzující kód je neplatný nebo mu vypršela platnost.
+invalid_code_forgot_password=Váš potvrzovací kód je neplatný nebo mu vypršela platnost. Klikněte zde pro vytvoření nového kódu.
invalid_password=Vaše heslo se neshoduje s heslem, které bylo použito k vytvoření účtu.
reset_password_helper=Obnovit účet
+reset_password_wrong_user=Jste přihlášen/a jako %s, ale odkaz pro obnovení účtu je pro %s
password_too_short=Délka hesla musí být minimálně %d znaků.
non_local_account=Externě ověřovaní uživatelé nemohou aktualizovat své heslo prostřednictvím webového rozhraní Gitea.
verify=Ověřit
@@ -390,6 +439,7 @@ openid_connect_title=Připojení k existujícímu účtu
openid_connect_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde.
openid_register_title=Vytvořit nový účet
openid_register_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde.
+openid_signin_desc=Zadejte vaši OpenID URI. Například: alice.openid.example.org nebo https://openid.example.org/alice.
disable_forgot_password_mail=Obnovení účtu je zakázáno, protože není nastaven žádný e-mail. Obraťte se na správce webu.
disable_forgot_password_mail_admin=Obnovení účtu je dostupné pouze po nastavení e-mailu. Pro povolení obnovy účtu nastavte prosím e-mail.
email_domain_blacklisted=Nemůžete se registrovat s vaší e-mailovou adresou.
@@ -399,8 +449,11 @@ authorize_application_created_by=Tuto aplikaci vytvořil %s.
authorize_application_description=Pokud povolíte přístup, bude moci přistupovat a zapisovat do všech vašich informací o účtu včetně soukromých repozitářů a organizací.
authorize_title=Autorizovat „%s“ pro přístup k vašemu účtu?
authorization_failed=Autorizace selhala
+authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný požadavek. Kontaktujte prosím správce aplikace, kterou jste se pokoušeli autorizovat.
sspi_auth_failed=SSPI autentizace selhala
+password_pwned=Heslo, které jste zvolili, je na seznamu odcizených hesel, která byla dříve odhalena při narušení veřejných dat. Zkuste to prosím znovu s jiným heslem.
password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned
+last_admin=Nelze odstranit posledního správce. Musí existovat alespoň jeden správce.
[mail]
view_it_on=Zobrazit na %s
@@ -414,6 +467,7 @@ activate_account.text_1=Ahoj %[1]s, děkujeme za registraci na %[2]s!
activate_account.text_2=Pro aktivaci vašeho účtu do %s klikněte na následující odkaz:
activate_email=Ověřte vaši e-mailovou adresu
+activate_email.title=%s, prosím ověřte vaši e-mailovou adresu
activate_email.text=Pro aktivaci vašeho účtu do %s klikněte na následující odkaz:
register_notify=Vítejte v Gitea
@@ -428,7 +482,7 @@ reset_password.text=Klikněte prosím na následující odkaz pro obnovení vaš
register_success=Registrace byla úspěšná
-issue_assigned.pull=@%[1]s vás přiřadil/a k požadavku na natažení %[2]s repozitáři %[3]s.
+issue_assigned.pull=@%[1]s vás přiřadil/a k pull requestu %[2]s v repozitáři %[3]s.
issue_assigned.issue=@%[1]s vás přiřadil/a k úkolu %[2]s repozitáři %[3]s.
issue.x_mentioned_you=@%s vás zmínil/a:
@@ -438,11 +492,11 @@ issue.action.push_n=@%[1]s nahrál/a %[3]d commity do %[2]s
issue.action.close=@%[1]s uzavřel/a #%[2]d.
issue.action.reopen=@%[1]s znovu otevřel/a #%[2]d.
issue.action.merge=@%[1]s sloučil/a #%[2]d do %[3]s.
-issue.action.approve=@%[1]s schválil/a tento požadavek na natažení.
-issue.action.reject=@%[1]s požadoval/a změny v tomto požadavku na natažení.
-issue.action.review=@%[1]s okomentoval/a tento požadavek na natažení.
-issue.action.review_dismissed=@%[1]s odmítl/a poslední kontrolu z %[2]s pro tento požadavek na natažení.
-issue.action.ready_for_review=@%[1]s označil/a tento požadavek na natažení jako připravený ke kontrole.
+issue.action.approve=@%[1]s schválil/a tento pull request.
+issue.action.reject=@%[1]s požadoval/a změny v tomto pull requestu.
+issue.action.review=@%[1]s okomentoval/a tento pull request.
+issue.action.review_dismissed=@%[1]s odmítl/a poslední kontrolu z %[2]s pro tento pull request.
+issue.action.ready_for_review=@%[1]s označil/a tento pull request jako připravený ke kontrole.
issue.action.new=@%[1]s vytvořil/a #%[2]d.
issue.in_tree_path=V %s:
@@ -509,6 +563,7 @@ url_error=`„%s“ není platná adresa URL.`
include_error=` musí obsahovat substring „%s“.`
glob_pattern_error=`zástupný vzor je neplatný: %s.`
regex_pattern_error=` regex vzor je neplatný: %s.`
+username_error=` může obsahovat pouze alfanumerické znaky („0-9“, „a-z“, „A-Z“), pomlčku („-“), podtržítka („_“) a tečka („.“). Nemůže začínat nebo končit nealfanumerickými znaky a po sobě jdoucí nealfanumerické znaky jsou také zakázány.`
invalid_group_team_map_error=` mapování je neplatné: %s`
unknown_error=Neznámá chyba:
captcha_incorrect=CAPTCHA kód není správný.
@@ -531,6 +586,7 @@ team_name_been_taken=Název týmu je již použit.
team_no_units_error=Povolit přístup alespoň do jedné sekce repozitáře.
email_been_used=Tato e-mailová adresa je již používána.
email_invalid=Emailová adresa je neplatná.
+email_domain_is_not_allowed=Doména uživatelského e-mailu %s je v rozporu s EMAIL_DOMAIN_ALLOWLIST nebo EMAIL_DOMAIN_BLOCKLIST. Ujistěte se, že je Vaše operace očekávána.
openid_been_used=OpenID addresa „%s“ je již použita.
username_password_incorrect=Uživatelské jméno nebo heslo není správné.
password_complexity=Heslo nesplňuje požadavky na složitost:
@@ -542,6 +598,8 @@ enterred_invalid_repo_name=Zadaný název repozitáře není správný.
enterred_invalid_org_name=Zadaný název organizace není správný.
enterred_invalid_owner_name=Nové jméno vlastníka není správné.
enterred_invalid_password=Zadané heslo není správné.
+unset_password=Přihlášený uživatel nenastavil heslo.
+unsupported_login_type=Typ přihlášení není podporován pro odstranění účtu.
user_not_exist=Tento uživatel neexistuje.
team_not_exist=Tento tým neexistuje.
last_org_owner=Nemůžete odstranit posledního uživatele z týmu „vlastníci“. Musí existovat alespoň jeden vlastník pro organizaci.
@@ -553,13 +611,22 @@ invalid_ssh_key=Nelze ověřit váš SSH klíč: %s
invalid_gpg_key=Nelze ověřit váš GPG klíč: %s
invalid_ssh_principal=Neplatný SSH Principal certifikát: %s
must_use_public_key=Zadaný klíč je soukromý klíč. Nenahrávejte svůj soukromý klíč nikde. Místo toho použijte váš veřejný klíč.
+unable_verify_ssh_key=Nelze ověřit váš SSH klíč.
auth_failed=Ověření selhalo: %v
+still_own_repo=Váš účet vlastní jeden nebo více repozitářů. Nejprve je smažte nebo převeďte.
+still_has_org=Váš účet je členem jedné nebo více organizací. Nejdříve je musíte opustit.
+still_own_packages=Váš účet vlastní jeden nebo více balíčků. Nejprve je musíte odstranit.
+org_still_own_repo=Organizace stále vlastní jeden nebo více repozitářů. Nejdříve je smažte nebo převeďte.
+org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. Nejdříve je smažte.
target_branch_not_exist=Cílová větev neexistuje.
+admin_cannot_delete_self=Nemůžete se smazat, dokud jste správce. Nejdříve prosím odeberte svá administrátorská oprávnění.
+
[user]
change_avatar=Změnit váš avatar…
+joined_on=Přidal/a se %s
repositories=Repozitáře
activity=Veřejná aktivita
followers=Sledující
@@ -575,10 +642,36 @@ user_bio=Životopis
disabled_public_activity=Tento uživatel zakázal veřejnou viditelnost aktivity.
email_visibility.limited=Vaše e-mailová adresa je viditelná pro všechny ověřené uživatele
email_visibility.private=Vaše e-mailová adresa je viditelná pouze pro vás a administrátory
+show_on_map=Zobrazit toto místo na mapě
+settings=Uživatelská nastavení
-form.name_reserved=Uživatelské jméno "%s" je rezervováno.
-form.name_pattern_not_allowed=Vzor "%s" není povolen v uživatelském jméně.
-form.name_chars_not_allowed=Uživatelské jméno "%s" obsahuje neplatné znaky.
+form.name_reserved=Uživatelské jméno „%s“ je rezervováno.
+form.name_pattern_not_allowed=Vzor „%s“ není povolen v uživatelském jméně.
+form.name_chars_not_allowed=Uživatelské jméno „%s“ obsahuje neplatné znaky.
+
+block.block=Blokovat
+block.block.user=Zablokovat Uživatele
+block.block.org=Blokovat uživatele pro organizaci
+block.block.failure=Nepodařilo se zablokovat uživatele: %s
+block.unblock=Odblokovat
+block.unblock.failure=Nepodařilo se odblokovat uživatele: %s
+block.blocked=Zablokovali jste tohoto uživatele.
+block.title=Zablokovat Uživatele
+block.info=Blokování uživatele brání v interakci s repozitáři, jako je otevírání nebo komentování pull requestů nebo úkolů. Další informace o blokování uživatele.
+block.info_1=Zablokování uživatele zabrání následujícím akcím na vašem účtu a repozirářích:
+block.info_2=sledují váš účet
+block.info_3=pošle vám oznámení pomocí @zmínění vašeho uživatelského jména
+block.info_4=pozváním vás jako spolupracovníka do jejich repozitářů
+block.info_5=oblíbení, rozštěpení nebo sledování repozitářů
+block.info_6=otevření a komentování úkolů nebo pull requestů
+block.info_7=reagovat na své komentáře v úkolech nebo pull requestů
+block.user_to_block=Uživatel k blokování
+block.note=Poznámka
+block.note.title=Volitelná poznámka:
+block.note.info=Poznámka není pro blokovaného uživatele viditelná.
+block.note.edit=Upravit poznámku
+block.list=Blokovaní uživatelé
+block.list.none=Nemáte blokované žádné uživatele.
[settings]
profile=Profil
@@ -596,9 +689,13 @@ delete=Smazat účet
twofa=Dvoufaktorové ověřování
account_link=Propojené účty
organization=Organizace
+uid=UID
webauthn=Bezpečnostní klíče
public_profile=Veřejný profil
+biography_placeholder=Řekněte nám něco o sobě! (Můžete použít Markdown)
+location_placeholder=Sdílejte svou přibližnou polohu s ostatními
+profile_desc=Nastavte, jak bude váš profil zobrazen ostatním uživatelům. Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla a operace Git.
password_username_disabled=Externí uživatelé nemohou měnit svoje uživatelské jméno. Kontaktujte prosím svého administrátora pro více detailů.
full_name=Celé jméno
website=Web
@@ -606,15 +703,20 @@ location=Místo
update_theme=Aktualizovat motiv vzhledu
update_profile=Aktualizovat profil
update_language=Aktualizovat jazyk
-update_language_not_found=Jazyk "%s" není k dispozici.
+update_language_not_found=Jazyk „%s“ není k dispozici.
update_language_success=Jazyk byl aktualizován.
update_profile_success=Váš profil byl aktualizován.
change_username=Vaše uživatelské jméno bylo změněno.
+change_username_prompt=Poznámka: Změna uživatelského jména také změní URL vašeho účtu.
+change_username_redirect_prompt=Staré uživatelské jméno bude přesměrováváno, dokud nebude znovu obsazeno.
continue=Pokračovat
cancel=Zrušit
language=Jazyk
ui=Motiv vzhledu
hidden_comment_types=Skryté typy komentářů
+hidden_comment_types_description=Zde zkontrolované typy komentářů nebudou zobrazeny na stránkách problémů. Zaškrtnutí „Štítek“ například odstraní všechny komentáře „ přidal/odstranil Administración: los miembros pueden ver, hacer push y añadir colaboradores a los repositorios del equipo.
teams.create_repo_permission_desc=Adicionalmente, este equipo concede permiso Crear repositorio: los miembros pueden crear nuevos repositorios en la organización.
teams.repositories=Repositorios del equipo
-teams.search_repo_placeholder=Buscar repositorio…
teams.remove_all_repos_title=Eliminar todos los repositorios del equipo
teams.remove_all_repos_desc=Esto eliminará todos los repositorios del equipo.
teams.add_all_repos_title=Añadir todos los repositorios
@@ -2686,6 +2661,8 @@ integrations=Integraciones
authentication=Orígenes de autenticación
emails=Correos de usuario
config=Configuración
+config_summary=Resumen
+config_settings=Configuración
notices=Notificaciones del sistema
monitor=Monitorización
first_page=Primera
@@ -2695,7 +2672,6 @@ settings=Configuración de Admin
dashboard.new_version_hint=Gitea %s ya está disponible, estás ejecutando %s. Revisa el blog para más detalles.
dashboard.statistic=Resumen
-dashboard.operations=Operaciones de mantenimiento
dashboard.system_status=Estado del sistema
dashboard.operation_name=Nombre de la operación
dashboard.operation_switch=Interruptor
@@ -2859,9 +2835,6 @@ repos.unadopted.no_more=No se encontraron más repositorios no adoptados
repos.owner=Propietario
repos.name=Nombre
repos.private=Privado
-repos.watches=Vigilantes
-repos.stars=Estrellas
-repos.forks=Forks
repos.issues=Incidencias
repos.size=Tamaño
repos.lfs_size=Tamaño LFS
@@ -2985,7 +2958,6 @@ auths.tip.nextcloud=`Registre un nuevo consumidor OAuth en su instancia usando e
auths.tip.dropbox=Crear nueva aplicación en https://www.dropbox.com/developers/apps
auths.tip.facebook=`Registre una nueva aplicación en https://developers.facebook.com/apps y agregue el producto "Facebook Login"`
auths.tip.github=Registre una nueva aplicación OAuth en https://github.com/settings/applications/new
-auths.tip.gitlab=Registrar nueva solicitud en https://gitlab.com/profile/applications
auths.tip.google_plus=Obtener credenciales de cliente OAuth2 desde la consola API de Google en https://console.developers.google.com/
auths.tip.openid_connect=Use el OpenID Connect Discovery URL (/.well-known/openid-configuration) para especificar los puntos finales
auths.tip.twitter=Ir a https://dev.twitter.com/apps, crear una aplicación y asegurarse de que la opción "Permitir que esta aplicación sea usada para iniciar sesión con Twitter" está activada
@@ -3199,6 +3171,7 @@ notices.desc=Descripción
notices.op=Operación
notices.delete_success=Los avisos del sistema se han eliminado.
+
[action]
create_repo=creó el repositorio %s
rename_repo=repositorio renombrado de %[1]s
a %[3]s
@@ -3383,6 +3356,8 @@ rpm.registry=Configurar este registro desde la línea de comandos:
rpm.distros.redhat=en distribuciones basadas en RedHat
rpm.distros.suse=en distribuciones basadas en SUSE
rpm.install=Para instalar el paquete, ejecute el siguiente comando:
+rpm.repository=Información del repositorio
+rpm.repository.architectures=Arquitecturas
rubygems.install=Para instalar el paquete usando gem, ejecute el siguiente comando:
rubygems.install2=o añádelo al archivo Gemfile:
rubygems.dependencies.runtime=Dependencias en tiempo de ejecución
@@ -3530,7 +3505,6 @@ variables.none=Aún no hay variables.
variables.deletion=Eliminar variable
variables.deletion.description=Eliminar una variable es permanente y no se puede deshacer. ¿Continuar?
variables.description=Las variables se pasarán a ciertas acciones y no se podrán leer de otro modo.
-variables.id_not_exist=Variable con id %d no existe.
variables.edit=Editar variable
variables.deletion.failed=No se pudo eliminar la variable.
variables.deletion.success=La variable ha sido eliminada.
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index c9099299a0..d19eb356d2 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -100,6 +100,15 @@ concept_user_organization=سازمان
name=نام
+filter=فیلتر
+filter.is_archived=بایگانی شده
+filter.is_template=قالب
+filter.public=عمومی
+filter.private=خصوصی
+
+
+[search]
+
[aria]
[heatmap]
@@ -233,7 +242,6 @@ collaborative_repos=مخازن همکاری
my_orgs=سازمان های من
my_mirrors=قرینههای من
view_home=نمایش %s
-search_repos=یافتن مخزن…
filter=فیلترهای دیگر
filter_by_team_repositories=فیلتر کردن با مخازن تیمها
feed_of=`خوراک از "%s"`
@@ -254,14 +262,7 @@ issues.in_your_repos=در مخازن شما
repos=مخازن
users=کاربران
organizations=سازمان ها
-search=جستجو
code=کد
-search.fuzzy=نادقیق
-search.match=تطابق
-repo_no_results=مخزنی مطابق با این مورد یافت نشد.
-user_no_results=کاربری مطابق با این مورد یافت نشد.
-org_no_results=سازمانی مطابق با این مورد یافت نشد.
-code_no_results=کد منبعی مطابق با جستجوی شما یافت نشد.
code_last_indexed_at=آخرین به روزرسانی در %s
[auth]
@@ -274,7 +275,6 @@ remember_me=این دستگاه را بخاطر بسپار
forgot_password_title=گذرواژه خود را فراموش کرده ام
forgot_password=گذرواژه خود را فراموش کردهاید؟
sign_up_now=نیاز به یک حساب دارید؟ هماکنون ثبت نام کنید.
-confirmation_mail_sent_prompt=ایمیل تاییدیه جدیدی به %s ارسال شد. لطفا صندوق ورودی خود را در %d ساعت آینده برای تکمیل فرایند ثبت نام بررسی کنید.
must_change_password=گذرواژه خود را به روز کنید
allow_password_change=نیاز به کاربر برای تغییرگذرواژه (توصیه می شود)
reset_password_mail_sent_prompt=ایمیل تاییدیه جدیدی به %s ارسال شد. لطفا صندوق ورودی خود را در %s آینده برای فرآیند بازیابی حساب کاربری خود بررسی کنید.
@@ -463,6 +463,7 @@ auth_failed=تشخیص هویت ناموفق: %v
target_branch_not_exist=شاخه مورد نظر وجود ندارد.
+
[user]
change_avatar=تغییر آواتار…
repositories=مخازن
@@ -479,6 +480,7 @@ user_bio=زندگینامه
disabled_public_activity=این کاربر نمایش عمومی فعالیت های خود را غیرفعال کرده است.
+
[settings]
profile=نمایه
account=حساب کاربری
@@ -590,7 +592,6 @@ gpg_invalid_token_signature=کلید GPG ارائه شده، امضا و ژتو
gpg_token_required=باید یک امضا برای ژتون زیر ارائه کنید
gpg_token=توکن
gpg_token_help=با این میتوانید یک امضاء بسازید:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=امضای GPG زرهپوش
key_signature_gpg_placeholder=با '-----BEGIN PGP SIGNATURE-----' شروع میشود
ssh_key_verified=کلید تأیید شده
@@ -729,7 +730,6 @@ fork_repo=انشعاب از مخزن
fork_from=انشعاب از
fork_visibility_helper=نمایان بودن مخزن منشعب شده غیر قابل تغییر است.
use_template=استفاده از این الگو
-clone_in_vsc=کلون کردن در VS Code
download_zip=دانلود ZIP
download_tar=دانلود TAR.GZ
download_bundle=بارگیری باندل
@@ -971,8 +971,6 @@ editor.require_signed_commit=شاخه یک کامیت امضا شده لازم
commits.desc=تاریخچه تغییرات کد منبع را مرور کنید.
commits.commits=کامیتها
commits.nothing_to_compare=این شاخه ها برابرند.
-commits.search=جستوجو کامیتها…
-commits.find=جستجو
commits.search_all=همه شاخه ها
commits.author=مولف
commits.message=پیام
@@ -1009,7 +1007,6 @@ projects.type.basic_kanban=پایه بر اساس سیستم کانبان (یک
projects.type.bug_triage=اشکال Triage
projects.template.desc=قالب پروژه
projects.template.desc_helper=برای شروع یک قالب پروژه را انتخاب کنید
-projects.type.uncategorized=دستهبندی نشده
projects.column.edit_title=نام
projects.column.new_title=نام
projects.column.color=رنگ
@@ -1300,7 +1297,6 @@ pulls.compare_compare=واکشی از
pulls.switch_comparison_type=سوئیچ نوع مقایسه
pulls.switch_head_and_base=سر و پایه سوئیچ
pulls.filter_branch=صافی شاخه
-pulls.no_results=هیچ نتیجهای یافت نشد.
pulls.nothing_to_compare=این شاخهها یکی هستند. نیازی به تقاضای واکشی نیست.
pulls.nothing_to_compare_and_allow_empty_pr=این شاخه ها برابر هستند. این PR خالی خواهد بود.
pulls.has_pull_request=`A درخواست pull بین این شاخه ها از قبل وجود دارد: %[2]s#%[3]d`
@@ -1498,12 +1494,7 @@ activity.git_stats_and_deletions=و
activity.git_stats_deletion_1=%d مذحوف
activity.git_stats_deletion_n=%d مذحوف
-search=جستجو
-search.search_repo=جستجوی مخزن
-search.fuzzy=درهم
-search.match=مطابق
-search.results=نتیجه جستجو برای "%s" در %s
-search.code_no_results=کد منبعی مطابق با جستجوی شما یافت نشد.
+contributors.contribution_type.commits=کامیتها
settings=تنظيمات
settings.desc=تنظیمات جایی است که شما میتوانید تنظیمات مخزن خود را مدیریت کنید
@@ -1620,7 +1611,6 @@ settings.delete_collaborator=حذف
settings.collaborator_deletion=حذفکردن همکار
settings.collaborator_deletion_desc=حذف یک همکار از مخزن دسترسیهای آنها را را مجدد لغو میکند. آیا ادامه میدهید؟
settings.remove_collaborator_success=همكار حذف شد.
-settings.search_user_placeholder=جستجوی کاربر…
settings.org_not_allowed_to_be_collaborator=سازمان ها را نمیتوان به عنوان همکار افزود.
settings.change_team_access_not_allowed=تغییر دسترسی های تیم برای این مخزن توسط مالک ارگان محدود شده است
settings.team_not_in_organization=تیم همانند ارگان برای این مخزن نیست
@@ -1628,7 +1618,6 @@ settings.teams=تیم ها
settings.add_team=افزودن تیم
settings.add_team_duplicate=تیم پیش از این مخزن داشته
settings.add_team_success=تیم هماکنون به مخزن دسترسی دارد.
-settings.search_team=جستجوی تیم…
settings.change_team_permission_tip=دسترسی تیم در صفحه تنظیمات تیم انجام شده و برای هر مخزن نمی تواند تغییر یابد
settings.delete_team_tip=این تیم به تمامی مخازن دسترسی دارد و نمی تواند حذف شود
settings.remove_team_success=دسترسی تیم به مخزن حذف شد.
@@ -1745,9 +1734,7 @@ settings.protect_whitelist_committers=لیست سفید برای درج محدو
settings.protect_whitelist_committers_desc=فقط به کاربران یا تیمهای موجود لیست سفید برای درج در این شاخه اجازه خواهند داشت (اما نه درج اجباری).
settings.protect_whitelist_deploy_keys=فهرست سفید کلیدهای استقرار با دسترسی نوشتن برای push کردن.
settings.protect_whitelist_users=کاربران لیست سفید برای درج در مخزن:
-settings.protect_whitelist_search_users=جستجوی کاربر…
settings.protect_whitelist_teams=تیمهای لیست سفید برای درج در مخزن:
-settings.protect_whitelist_search_teams=جستجوی تیم ها…
settings.protect_merge_whitelist_committers=فعال کردن لیست سفید ادغام
settings.protect_merge_whitelist_committers_desc=اجازه به کاربران یا تیمهای موجود لیست سفید برای تقاضا ادغام واکشی در این شاخه.
settings.protect_merge_whitelist_users=کاربران لیست سفید برای ادغام:
@@ -1951,6 +1938,8 @@ error.csv.too_large=نمی توان این فایل را رندر کرد زیر
error.csv.unexpected=نمی توان این فایل را رندر کرد زیرا حاوی یک کاراکتر غیرمنتظره در خط %d و ستون %d است.
error.csv.invalid_field_count=نمی توان این فایل را رندر کرد زیرا تعداد فیلدهای آن در خط %d اشتباه است.
+[graphs]
+
[org]
org_name_holder=نام سازمان
org_full_name_holder=نام کامل سازمان
@@ -2042,7 +2031,6 @@ teams.write_permission_desc=این تیم دسترسی نوشتننوشتن خواهد داشت: اعضا خواهند توانست مخازن تیم را خوانده ، تغییراتی در آنها اعمال کرده و یا همکارانشان را به مخازن اضافه نمایند.
teams.create_repo_permission_desc=علاوه بر این ، این تیم اجازه ساخت مخزن دسترسی : اعضا می توانند مخازن جدیدی را در سازمان ایجاد کنند.
teams.repositories=مخازن تیم
-teams.search_repo_placeholder=جستجوی مخزن...
teams.remove_all_repos_title=حذف تمام مخازن تیم
teams.remove_all_repos_desc=با این کار همه مخازن از تیم حذف می شوند.
teams.add_all_repos_title=افزودن همه مخازن
@@ -2067,6 +2055,8 @@ hooks=وب هوک ها
authentication=منابع احراز هویت
emails=ایمیل های کاربر
config=پیکربندی
+config_summary=چکیده
+config_settings=تنظيمات
notices=هشدارهای سامانه
monitor=نظارت
first_page=نخستین
@@ -2074,7 +2064,6 @@ last_page=واپسین
total=مجموع: %d
dashboard.statistic=چکیده
-dashboard.operations=عملیاتهای نگهداری
dashboard.system_status=وضعیت سامانه
dashboard.operation_name=نام عملیات
dashboard.operation_switch=تعویض
@@ -2215,9 +2204,6 @@ repos.unadopted.no_more=هیچ مخزن تایید نشده دیگری یافت
repos.owner=مالک
repos.name=نام
repos.private=خصوصی
-repos.watches=تماشا شده
-repos.stars=ستاره ها
-repos.forks=انشعابها
repos.issues=مسائل
repos.size=اندازه
@@ -2316,7 +2302,6 @@ auths.tip.nextcloud=با استفاده از منوی زیر "تنظیمات ->
auths.tip.dropbox=یک برنامه جدید در https://www.dropbox.com/developers/apps بسازید
auths.tip.facebook=`یک برنامه جدید در https://developers.facebook.com/apps بسازید برای ورود از طریق فیس بوک قسمت محصولات "Facebook Login"`
auths.tip.github=یک برنامه OAuth جدید در https://github.com/settings/applications/new ثبت کنید
-auths.tip.gitlab=ثبت یک برنامه جدید در https://gitlab.com/profile/applications
auths.tip.google_plus=اطلاعات مربوط به مشتری OAuth2 را از کلاینت API Google در https://console.developers.google.com/
auths.tip.openid_connect=برای مشخص کردن نقاط پایانی از آدرس OpenID Connect Discovery URL ( /.well-known/openid-configuration) استفاده کنید.
auths.tip.twitter=به https://dev.twitter.com/apps بروید ، برنامه ای ایجاد کنید و اطمینان حاصل کنید که گزینه "اجازه استفاده از این برنامه برای ورود به سیستم با Twitter" را فعال کنید
@@ -2501,6 +2486,7 @@ notices.desc=توضیحات
notices.op=عملیات.
notices.delete_success=گزارش سیستم حذف شده است.
+
[action]
create_repo=مخزن ایجاد شده %s
rename_repo=مخزن تغییر نام داد از %[1]s
به %[3]s
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index b6abb49a35..f283209908 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -114,6 +114,15 @@ concept_user_organization=Organisaatio
name=Nimi
+filter=Suodata
+filter.is_archived=Arkistoidut
+filter.is_template=Malli
+filter.public=Julkinen
+filter.private=Yksityinen
+
+
+[search]
+
[aria]
[heatmap]
@@ -243,7 +252,6 @@ collaborative_repos=Yhteistyö repot
my_orgs=Organisaationi
my_mirrors=Peilini
view_home=Näytä %s
-search_repos=Etsi repo…
filter=Muut suodattimet
filter_by_team_repositories=Suodata tiimin repojen mukaan
feed_of=`Syöte "%s"`
@@ -264,13 +272,7 @@ issues.in_your_repos=Repoissasi
repos=Repot
users=Käyttäjät
organizations=Organisaatiot
-search=Hae
code=Koodi
-search.match=Osuma
-repo_no_results=Vastaavia repoja ei löydy.
-user_no_results=Vastaavia käyttäjiä ei löytynyt.
-org_no_results=Ei löytynyt vastaavia organisaatioita.
-code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt.
code_last_indexed_at=Viimeksi indeksoitu %s
[auth]
@@ -283,7 +285,6 @@ remember_me=Muista tämä laite
forgot_password_title=Unohtuiko salasana
forgot_password=Unohtuiko salasana?
sign_up_now=Tarvitsetko tilin? Rekisteröidy nyt.
-confirmation_mail_sent_prompt=Uusi varmistussähköposti on lähetetty osoitteeseen %s, ole hyvä ja tarkista saapuneet seuraavan %s tunnin sisällä saadaksesi rekisteröintiprosessin valmiiksi.
must_change_password=Vaihda salasanasi
allow_password_change=Vaadi käyttäjää vaihtamaan salasanansa (suositeltava)
reset_password_mail_sent_prompt=Varmistussähköposti on lähetetty osoitteeseen %s. Tarkista saapuneet seuraavan %s tunnin sisällä saadaksesi tilin palauttamisen valmiiksi.
@@ -425,6 +426,7 @@ auth_failed=Todennus epäonnistui: %v
target_branch_not_exist=Kohde branchia ei ole olemassa.
+
[user]
change_avatar=Vaihda profiilikuvasi…
repositories=Repot
@@ -439,6 +441,7 @@ unfollow=Lopeta seuraaminen
user_bio=Elämäkerta
+
[settings]
profile=Profiili
account=Tili
@@ -554,7 +557,6 @@ gpg_key_verify=Vahvista
gpg_token_required=Sinun täytyy antaa allekirjoitus alla olevalle pääsymerkille
gpg_token=Pääsymerkki
gpg_token_help=Voit luoda allekirjoituksen käyttäen:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Panssaroitu GPG-allekirjoitus
key_signature_gpg_placeholder=Alkaa sanoilla '-----BEGIN PGP SIGNATURE-----'
ssh_key_verified=Vahvistettu avain
@@ -656,7 +658,6 @@ visibility_helper_forced=Sivuston ylläpitäjä pakottaa uudet repot olemaan yks
fork_repo=Forkkaa repo
fork_from=Forkkaa lähteestä
fork_visibility_helper=Forkatun repon näkyvyyttä ei voi muuttaa.
-clone_in_vsc=Kloonaa VS Codessa
download_zip=Lataa ZIP
download_tar=Lataa TAR.GZ
repo_desc=Kuvaus
@@ -778,7 +779,6 @@ editor.require_signed_commit=Haara vaatii vahvistetun commitin
commits.commits=Commitit
commits.nothing_to_compare=Nämä haarat vastaavat toisiaan.
-commits.find=Haku
commits.search_all=Kaikki haarat
commits.author=Tekijä
commits.message=Viesti
@@ -805,7 +805,6 @@ projects.edit=Muokkaa projektia
projects.modify=Päivitä projekti
projects.type.basic_kanban=Yksinkertainen Kanban
projects.template.desc=Malli
-projects.type.uncategorized=Luokittelematon
projects.column.edit_title=Nimi
projects.column.new_title=Nimi
projects.open=Avaa
@@ -982,7 +981,6 @@ pulls.has_viewed_file=Katsottu
pulls.viewed_files_label=%[1]d / %[2]d tiedostoa katsottu
pulls.compare_compare=vedä kohteesta
pulls.filter_branch=Suodata branch
-pulls.no_results=Tuloksia ei löytynyt.
pulls.nothing_to_compare=Nämä haarat vastaavat toisiaan. Ei ole tarvetta luoda vetopyyntöä.
pulls.nothing_to_compare_and_allow_empty_pr=Nämä haarat vastaavat toisiaan. Vetopyyntö tulee olemaan tyhjä.
pulls.has_pull_request=`Vetopyyntö haarojen välillä on jo olemassa: %[2]s#%[3]d`
@@ -1074,9 +1072,7 @@ activity.git_stats_and_deletions=ja
activity.git_stats_deletion_1=%d poisto
activity.git_stats_deletion_n=%d poistoa
-search=Haku
-search.match=Osuma
-search.code_no_results=Hakuehtoasi vastaavaa lähdekoodia ei löytynyt.
+contributors.contribution_type.commits=Commitit
settings=Asetukset
settings.options=Repo
@@ -1116,7 +1112,6 @@ settings.delete_desc=Repon poistaminen on pysyvä eikä voi peruuttaa.
settings.delete_notices_1=- Tätä toimintoa EI VOI peruuttaa myöhemmin.
settings.update_settings_success=Repon asetukset on päivitetty.
settings.delete_collaborator=Poista
-settings.search_user_placeholder=Etsi käyttäjä…
settings.teams=Tiimit
settings.add_team=Lisää tiimi
settings.add_webhook=Lisää webkoukku
@@ -1200,7 +1195,6 @@ settings.branch_protection=Haaran '%s' suojaus
settings.protect_this_branch=Ota haaran suojaus käyttöön
settings.protect_whitelist_deploy_keys=Lisää julkaisuavaimet sallittujen listalle mahdollistaaksesi repohin kirjoituksen.
settings.protect_whitelist_users=Lista käyttäjistä joilla työntö oikeus:
-settings.protect_whitelist_search_users=Etsi käyttäjiä…
settings.protect_merge_whitelist_committers_desc=Salli vain listaan merkittyjen käyttäjien ja tiimien yhdistää vetopyynnöt tähän haaraan.
settings.protect_merge_whitelist_users=Lista käyttäjistä joilla yhdistämis-oikeus:
settings.protect_required_approvals=Vaadittavat hyväksynnät:
@@ -1314,6 +1308,8 @@ topic.done=Valmis
+[graphs]
+
[org]
org_name_holder=Organisaatio
org_full_name_holder=Organisaation täydellinen nimi
@@ -1402,6 +1398,8 @@ repositories=Repot
authentication=Todennuslähteet
emails=Käyttäjien sähköpostit
config=Asetukset
+config_summary=Yhteenveto
+config_settings=Asetukset
notices=Järjestelmän ilmoitukset
monitor=Valvonta
first_page=Ensimmäinen
@@ -1409,7 +1407,6 @@ last_page=Viimeisin
total=Yhteensä: %d
dashboard.statistic=Yhteenveto
-dashboard.operations=Huoltotoimet
dashboard.system_status=Järjestelmän tila
dashboard.operation_name=Toiminnon nimi
dashboard.operation_switch=Vaihda
@@ -1503,9 +1500,6 @@ repos.repo_manage_panel=Repojen hallinta
repos.owner=Omistaja
repos.name=Nimi
repos.private=Yksityinen
-repos.watches=Tarkkailijat
-repos.stars=Tähdet
-repos.forks=Haarat
repos.issues=Ongelmat
repos.size=Koko
@@ -1659,6 +1653,7 @@ notices.type_1=Repo
notices.desc=Kuvaus
notices.op=Toiminta
+
[action]
create_repo=luotu repo %s
rename_repo=uudelleennimetty repo %[1]s
nimelle %[3]s
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index f3a264c1c8..dc66402901 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -123,6 +123,7 @@ pin=Épingler
unpin=Désépingler
artifacts=Artefacts
+confirm_delete_artifact=Êtes-vous sûr de vouloir supprimer l‘artefact « %s » ?
archived=Archivé
@@ -141,6 +142,15 @@ confirm_delete_selected=Êtes-vous sûr de vouloir supprimer tous les éléments
name=Nom
value=Valeur
+filter=Filtrer
+filter.is_archived=Archivé
+filter.is_template=Modèle
+filter.public=Public
+filter.private=Privé
+
+
+[search]
+
[aria]
navbar=Barre de navigation
footer=Pied de page
@@ -314,7 +324,6 @@ collaborative_repos=Dépôts collaboratifs
my_orgs=Mes organisations
my_mirrors=Mes miroirs
view_home=Voir %s
-search_repos=Trouver un dépôt …
filter=Autres filtres
filter_by_team_repositories=Dépôts filtrés par équipe
feed_of=Flux de « %s »
@@ -335,20 +344,8 @@ issues.in_your_repos=Dans vos dépôts
repos=Dépôts
users=Utilisateurs
organizations=Organisations
-search=Rechercher
go_to=Atteindre
code=Code
-search.type.tooltip=Type de recherche
-search.fuzzy=Approximative
-search.fuzzy.tooltip=Inclure également les résultats proches de la recherche
-search.match=Exacte
-search.match.tooltip=Inclure uniquement les résultats exacts
-code_search_unavailable=Actuellement, la recherche de code n'est pas disponible. Veuillez contacter l'administrateur de votre site.
-repo_no_results=Aucun dépôt correspondant n'a été trouvé.
-user_no_results=Aucun utilisateur correspondant n'a été trouvé.
-org_no_results=Aucune organisation correspondante n'a été trouvée.
-code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
-code_search_results=Résultats de la recherche pour « %s »
code_last_indexed_at=Dernière indexation %s
relevant_repositories_tooltip=Les dépôts qui sont des forks ou qui n'ont aucun sujet, aucune icône et aucune description sont cachés.
relevant_repositories=Seuls les dépôts pertinents sont affichés, afficher les résultats non filtrés.
@@ -361,11 +358,11 @@ disable_register_prompt=Les inscriptions sont désactivées. Veuillez contacter
disable_register_mail=La confirmation par courriel à l’inscription est désactivée.
manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation.
remember_me=Mémoriser cet appareil
+remember_me.compromised=Le jeton de connexion n’est plus valide, ce qui peut indiquer un compte compromis. Veuillez inspecter les activités inhabituelles de votre compte.
forgot_password_title=Mot de passe oublié
forgot_password=Mot de passe oublié ?
sign_up_now=Pas de compte ? Inscrivez-vous maintenant.
sign_up_successful=Le compte a été créé avec succès. Bienvenue !
-confirmation_mail_sent_prompt=Un nouveau mail de confirmation a été envoyé à %s. Veuillez vérifier votre boîte de réception dans les prochaines %s pour valider votre enregistrement.
must_change_password=Réinitialisez votre mot de passe
allow_password_change=Demande à l'utilisateur de changer son mot de passe (recommandé)
reset_password_mail_sent_prompt=Un mail de confirmation a été envoyé à %s. Veuillez vérifier votre boîte de réception dans les prochaines %s pour terminer la procédure de récupération du compte.
@@ -422,6 +419,7 @@ authorization_failed_desc=L'autorisation a échoué car nous avons détecté une
sspi_auth_failed=Échec de l'authentification SSPI
password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste des mots de passe ayant fuité sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs.
password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
+last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis.
[mail]
view_it_on=Voir sur %s
@@ -587,6 +585,8 @@ org_still_own_packages=Cette organisation possède encore un ou plusieurs paquet
target_branch_not_exist=La branche cible n'existe pas.
+admin_cannot_delete_self=Vous ne pouvez pas vous supprimer vous-même lorsque vous êtes admin. Veuillez d’abord supprimer vos privilèges d’administrateur.
+
[user]
change_avatar=Changer votre avatar…
joined_on=Inscrit le %s
@@ -612,6 +612,7 @@ form.name_reserved=Le nom d’utilisateur "%s" est réservé.
form.name_pattern_not_allowed=Le motif « %s » n’est pas autorisé dans un nom de d'utilisateur.
form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides.
+
[settings]
profile=Profil
account=Compte
@@ -756,7 +757,6 @@ gpg_invalid_token_signature=La clé GPG, la signature et le jeton fournis ne cor
gpg_token_required=Vous devez fournir une signature pour le jeton ci-dessous
gpg_token=Jeton
gpg_token_help=Vous pouvez générer une signature en utilisant :
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Signature GPG renforcée
key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----'
verify_gpg_key_success=La clé GPG "%s" a été vérifiée.
@@ -793,7 +793,7 @@ valid_until_date=Valable jusqu'au %s
valid_forever=Valide pour toujours
last_used=Dernière utilisation le
no_activity=Aucune activité récente
-can_read_info=Lue(s)
+can_read_info=Lecture
can_write_info=Écriture
key_state_desc=Cette clé a été utilisée au cours des 7 derniers jours
token_state_desc=Ce jeton a été utilisé au cours des 7 derniers jours
@@ -826,7 +826,7 @@ permissions_public_only=Publique uniquement
permissions_access_all=Tout (public, privé et limité)
select_permissions=Sélectionner les autorisations
permission_no_access=Aucun accès
-permission_read=Lue(s)
+permission_read=Lecture
permission_write=Lecture et écriture
access_token_desc=Les autorisations des jetons sélectionnées se limitent aux routes API correspondantes. Lisez la documentation pour plus d’informations.
at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton.
@@ -950,7 +950,6 @@ fork_branch=Branche à cloner sur la bifurcation
all_branches=Toutes les branches
fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il n’a pas de propriétaire valide.
use_template=Utiliser ce modèle
-clone_in_vsc=Cloner dans VS Code
download_zip=Télécharger le ZIP
download_tar=Télécharger le TAR.GZ
download_bundle=Télécharger le BUNDLE
@@ -966,6 +965,8 @@ issue_labels_helper=Sélectionner un jeu de label.
license=Licence
license_helper=Sélectionner une licence
license_helper_desc=Une licence réglemente ce que les autres peuvent ou ne peuvent pas faire avec votre code. Vous ne savez pas laquelle est la bonne pour votre projet ? Comment choisir une licence.
+object_format=Format d'objet
+object_format_helper=Format d’objet pour ce dépôt. Ne peut être modifié plus tard. SHA1 est le plus compatible.
readme=LISEZMOI
readme_helper=Choisissez un modèle de fichier LISEZMOI.
readme_helper_desc=Le README est l'endroit idéal pour décrire votre projet et accueillir des contributeurs.
@@ -983,6 +984,7 @@ mirror_prune=Purger
mirror_prune_desc=Supprimer les références externes obsolètes
mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s)
mirror_interval_invalid=L'intervalle de synchronisation est invalide.
+mirror_sync=synchronisé
mirror_sync_on_commit=Synchroniser quand les révisions sont soumis
mirror_address=Cloner depuis une URL
mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation.
@@ -1033,6 +1035,7 @@ desc.public=Publique
desc.template=Modèle
desc.internal=Interne
desc.archived=Archivé
+desc.sha256=SHA256
template.items=Élément du modèle
template.git_content=Contenu Git (branche par défaut)
@@ -1183,6 +1186,8 @@ audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « au
stored_lfs=Stocké avec Git LFS
symbolic_link=Lien symbolique
executable_file=Fichiers exécutables
+vendored=Externe
+generated=Générée
commit_graph=Graphe des révisions
commit_graph.select=Sélectionner les branches
commit_graph.hide_pr_refs=Masquer les demandes d'ajout
@@ -1269,9 +1274,7 @@ commits.desc=Naviguer dans l'historique des modifications.
commits.commits=Révisions
commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents.
commits.nothing_to_compare=Ces branches sont égales.
-commits.search=Rechercher des révisions…
commits.search.tooltip=Vous pouvez utiliser les mots-clés "author:", "committer:", "after:", ou "before:" pour filtrer votre recherche, ex.: "revert author:Alice before:2019-01-13".
-commits.find=Chercher
commits.search_all=Toutes les branches
commits.author=Auteur
commits.message=Message
@@ -1322,7 +1325,6 @@ projects.type.basic_kanban=Kanban basique
projects.type.bug_triage=Bug à trier
projects.template.desc=Modèle de projet
projects.template.desc_helper=Sélectionnez un modèle de projet pour débuter
-projects.type.uncategorized=Non catégorisé
projects.column.edit=Modifier la colonne
projects.column.edit_title=Nom
projects.column.new_title=Nom
@@ -1330,10 +1332,7 @@ projects.column.new_submit=Créer une colonne
projects.column.new=Nouvelle colonne
projects.column.set_default=Définir par défaut
projects.column.set_default_desc=Les tickets et demandes d’ajout non-catégorisés seront placés dans cette colonne.
-projects.column.unset_default=Défaire par défaut
-projects.column.unset_default_desc=Les tickets et demandes d'ajouts non-catégorisés seront placés dans une colonne idoine.
projects.column.delete=Supprimer la colonne
-projects.column.deletion_desc=La suppression d'une colonne de projet déplace tous les tickets liés à 'Non catégorisé'. Continuer ?
projects.column.color=Couleur
projects.open=Ouvrir
projects.close=Fermer
@@ -1445,7 +1444,6 @@ issues.filter_sort.moststars=Favoris (décroissant)
issues.filter_sort.feweststars=Favoris (croissant)
issues.filter_sort.mostforks=Bifurcations (décroissant)
issues.filter_sort.fewestforks=Bifurcations (croissant)
-issues.keyword_search_unavailable=La recherche par mot clé n'est pas disponible. Veuillez contacter l'administrateur de votre instance Gitea.
issues.action_open=Ouvrir
issues.action_close=Fermer
issues.action_label=Label
@@ -1697,7 +1695,6 @@ pulls.compare_compare=tirer les modifications depuis
pulls.switch_comparison_type=Changer le type de comparaison
pulls.switch_head_and_base=Passez de head à base
pulls.filter_branch=Filtre de branche
-pulls.no_results=Aucun résultat trouvé.
pulls.show_all_commits=Afficher toutes les révisions
pulls.show_changes_since_your_last_review=Affiche les modifications depuis votre dernière évaluation.
pulls.showing_only_single_commit=Affiche uniquement les changements de la révision %[1]s
@@ -1706,6 +1703,7 @@ pulls.select_commit_hold_shift_for_range=Maintenir Maj et cliquer sur des révis
pulls.review_only_possible_for_full_diff=Une évaluation n'est possible que lorsque vous affichez le différentiel complet.
pulls.filter_changes_by_commit=Filtrer par révision
pulls.nothing_to_compare=Ces branches sont identiques. Il n’y a pas besoin de créer une demande d'ajout.
+pulls.nothing_to_compare_have_tag=Les branches/étiquettes sélectionnées sont équivalentes.
pulls.nothing_to_compare_and_allow_empty_pr=Ces branches sont égales. Cette demande d'ajout sera vide.
pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux branches : %[2]s#%[3]d'
pulls.create=Créer une demande d'ajout
@@ -1764,6 +1762,7 @@ pulls.merge_pull_request=Créer une révision de fusion
pulls.rebase_merge_pull_request=Rebaser puis avancer rapidement
pulls.rebase_merge_commit_pull_request=Rebaser puis créer une révision de fusion
pulls.squash_merge_pull_request=Créer une révision de concaténation
+pulls.fast_forward_only_merge_pull_request=Avance rapide uniquement
pulls.merge_manually=Fusionner manuellement
pulls.merge_commit_id=L'ID de la révision de fusion
pulls.require_signed_wont_sign=La branche nécessite des révisions signées mais cette fusion ne sera pas signée
@@ -1900,6 +1899,7 @@ wiki.page_name_desc=Entrez un nom pour cette page Wiki. Certains noms spéciaux
wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial.
activity=Activité
+activity.navbar.contributors=Contributeurs
activity.period.filter_label=Période :
activity.period.daily=1 jour
activity.period.halfweekly=3 jours
@@ -1965,16 +1965,10 @@ activity.git_stats_and_deletions=et
activity.git_stats_deletion_1=%d suppression
activity.git_stats_deletion_n=%d suppressions
-search=Chercher
-search.search_repo=Rechercher dans le dépôt
-search.type.tooltip=Type de recherche
-search.fuzzy=Approximative
-search.fuzzy.tooltip=Inclure également les résultats proches de la recherche
-search.match=Exacte
-search.match.tooltip=Inclure uniquement les résultats exacts
-search.results=Résultats de la recherche « %s » dans %s
-search.code_no_results=Aucun code source correspondant à votre terme de recherche n'a été trouvé.
-search.code_search_unavailable=Actuellement, la recherche de code n'est pas disponible. Veuillez contacter l'administrateur de votre site.
+contributors.contribution_type.filter_label=Type de contribution :
+contributors.contribution_type.commits=Révisions
+contributors.contribution_type.additions=Ajouts
+contributors.contribution_type.deletions=Suppressions
settings=Paramètres
settings.desc=Les paramètres sont l'endroit où gérer les options du dépôt
@@ -2002,6 +1996,7 @@ settings.mirror_settings.docs.doc_link_title=Comment mettre en miroir les dépô
settings.mirror_settings.docs.doc_link_pull_section=la section « Pulling from a remote repository » de la documentation.
settings.mirror_settings.docs.pulling_remote_title=Tirer depuis un dépôt distant
settings.mirror_settings.mirrored_repository=Dépôt en miroir
+settings.mirror_settings.pushed_repository=Dépôt sortant
settings.mirror_settings.direction=Direction
settings.mirror_settings.direction.pull=Tirer
settings.mirror_settings.direction.push=Soumission
@@ -2053,6 +2048,7 @@ settings.pulls.default_allow_edits_from_maintainers=Autoriser les modifications
settings.releases_desc=Activer les publications du dépôt
settings.packages_desc=Activer le registre des paquets du dépôt
settings.projects_desc=Activer les projets de dépôt
+settings.projects_mode_all=Tous les projets
settings.actions_desc=Activer les actions du dépôt
settings.admin_settings=Paramètres administrateur
settings.admin_enable_health_check=Activer les vérifications de santé du dépôt (git fsck)
@@ -2127,7 +2123,6 @@ settings.delete_collaborator=Supprimer
settings.collaborator_deletion=Supprimer le collaborateur
settings.collaborator_deletion_desc=La suppression d'un collaborateur révoque son accès à ce dépôt. Continuer ?
settings.remove_collaborator_success=Le collaborateur a été retiré.
-settings.search_user_placeholder=Rechercher un utilisateur…
settings.org_not_allowed_to_be_collaborator=Les organisations ne peuvent être ajoutées en tant que collaborateur.
settings.change_team_access_not_allowed=La modification de l'accès de l'équipe au dépôt a été limitée au propriétaire de l'organisation
settings.team_not_in_organization=L'équipe n'est pas dans la même organisation que le dépôt
@@ -2135,7 +2130,6 @@ settings.teams=Équipes
settings.add_team=Ajouter une équipe
settings.add_team_duplicate=L'équipe a déjà le dépôt
settings.add_team_success=L'équipe a maintenant accès au dépôt.
-settings.search_team=Rechercher une équipe…
settings.change_team_permission_tip=La permission de l'équipe est définie sur la page de configuration de l'équipe et ne peut pas être modifiée par dépôt
settings.delete_team_tip=Cette équipe a accès à tous les dépôts et ne peut pas être supprimée
settings.remove_team_success=L'accès de l'équipe au dépôt a été supprimé.
@@ -2288,9 +2282,7 @@ settings.protect_whitelist_committers=Liste blanche des soumissions
settings.protect_whitelist_committers_desc=Seuls les utilisateurs ou les équipes autorisés pourront soumettre sur cette branche (sans forcer).
settings.protect_whitelist_deploy_keys=Mettez les clés de déploiement sur liste blanche avec accès en écriture pour soumettre.
settings.protect_whitelist_users=Utilisateurs sur liste blanche :
-settings.protect_whitelist_search_users=Rechercher des utilisateurs…
settings.protect_whitelist_teams=Équipes sur liste blanche :
-settings.protect_whitelist_search_teams=Rechercher des équipes…
settings.protect_merge_whitelist_committers=Activer la liste blanche pour la fusion
settings.protect_merge_whitelist_committers_desc=N'autoriser que les utilisateurs et les équipes en liste blanche d'appliquer les demandes de fusion sur cette branche.
settings.protect_merge_whitelist_users=Utilisateurs en liste blanche de fusion :
@@ -2311,6 +2303,8 @@ settings.protect_approvals_whitelist_users=Évaluateurs autorisés :
settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés :
settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées
settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande d’ajout, les approbations existantes sont révoquées.
+settings.ignore_stale_approvals=Ignorer les approbations obsolètes
+settings.ignore_stale_approvals_desc=Ignorer les approbations d’anciennes révisions (évaluations obsolètes) du décompte des approbations de la demande d’ajout. Non pertinent quand les évaluations obsolètes sont déjà révoquées.
settings.require_signed_commits=Exiger des révisions signées
settings.require_signed_commits_desc=Rejeter les soumissions sur cette branche lorsqu'ils ne sont pas signés ou vérifiables.
settings.protect_branch_name_pattern=Motif de nom de branche protégé
@@ -2366,6 +2360,7 @@ settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt.
settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir.
settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé.
settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé.
+settings.archive.mirrors_unavailable=Les miroirs ne sont pas disponibles lorsque le dépôt est archivé.
settings.unarchive.button=Réhabiliter
settings.unarchive.header=Réhabiliter ce dépôt
settings.unarchive.text=Réhabiliter un dépôt dégèle les actions de révisions et de soumissions, la gestion des tickets et des demandes d'ajouts.
@@ -2532,7 +2527,6 @@ branch.default_deletion_failed=La branche "%s" est la branche par défaut. Elle
branch.restore=`Restaurer la branche "%s"`
branch.download=`Télécharger la branche "%s"`
branch.rename=`Renommer la branche "%s"`
-branch.search=Rechercher une branche
branch.included_desc=Cette branche fait partie de la branche par défaut
branch.included=Incluses
branch.create_new_branch=Créer une branche à partir de la branche :
@@ -2564,6 +2558,13 @@ error.csv.too_large=Impossible de visualiser le fichier car il est trop volumine
error.csv.unexpected=Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d.
error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.
+[graphs]
+component_loading=Chargement de %s…
+component_loading_failed=Impossible de charger %s.
+component_loading_info=Ça prend son temps…
+component_failed_to_load=Une erreur inattendue s’est produite.
+contributors.what=contributions
+
[org]
org_name_holder=Nom de l'organisation
org_full_name_holder=Nom complet de l'organisation
@@ -2668,7 +2669,6 @@ teams.write_permission_desc=Cette équipe permet l'accès en écriture
teams.admin_permission_desc=Cette équipe permet l'accès administrateur : les membres peuvent voir, participer et ajouter des collaborateurs à ses dépôts.
teams.create_repo_permission_desc=De plus, cette équipe accorde la permission Créer un dépôt : les membres peuvent créer de nouveaux dépôts dans l'organisation.
teams.repositories=Dépôts de l'Équipe
-teams.search_repo_placeholder=Rechercher dans le dépôt…
teams.remove_all_repos_title=Supprimer tous les dépôts de l'équipe
teams.remove_all_repos_desc=Ceci supprimera tous les dépôts de l'équipe.
teams.add_all_repos_title=Ajouter tous les dépôts
@@ -2690,6 +2690,7 @@ teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindr
[admin]
dashboard=Tableau de bord
+self_check=Autodiagnostique
identity_access=Identité et accès
users=Comptes utilisateurs
organizations=Organisations
@@ -2700,6 +2701,8 @@ integrations=Intégrations
authentication=Sources d'authentification
emails=Emails de l'utilisateur
config=Configuration
+config_summary=Résumé
+config_settings=Paramètres
notices=Informations
monitor=Surveillance
first_page=Première
@@ -2709,7 +2712,6 @@ settings=Paramètres administrateur
dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez le blog pour plus de détails.
dashboard.statistic=Résumé
-dashboard.operations=Opérations de maintenance
dashboard.system_status=État du système
dashboard.operation_name=Nom de l'Opération
dashboard.operation_switch=Basculer
@@ -2735,6 +2737,7 @@ dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git
dashboard.delete_missing_repos.started=Tâche de suppression de tous les dépôts sans fichiers Git démarrée.
dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés
dashboard.sync_repo_branches=Synchroniser les branches manquantes depuis Git vers la base de donnée.
+dashboard.sync_repo_tags=Synchroniser les étiquettes git depuis les dépôts vers la base de données
dashboard.update_mirrors=Actualiser les miroirs
dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts
dashboard.check_repo_stats=Voir les statistiques de tous les dépôts
@@ -2789,6 +2792,7 @@ dashboard.stop_endless_tasks=Arrêter les tâches sans fin
dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés
dashboard.start_schedule_tasks=Démarrer les tâches planifiées
dashboard.sync_branch.started=Début de la synchronisation des branches
+dashboard.sync_tag.started=Synchronisation des étiquettes
dashboard.rebuild_issue_indexer=Reconstruire l’indexeur des tickets
users.user_manage_panel=Gestion du compte utilisateur
@@ -2874,9 +2878,6 @@ repos.unadopted.no_more=Aucun dépôt dépossédé trouvé.
repos.owner=Propriétaire
repos.name=Nom
repos.private=Privé
-repos.watches=Suivi par
-repos.stars=Votes
-repos.forks=Bifurcations
repos.issues=Tickets
repos.size=Taille
repos.lfs_size=Taille LFS
@@ -3001,7 +3002,6 @@ auths.tip.nextcloud=`Enregistrez un nouveau consommateur OAuth sur votre instanc
auths.tip.dropbox=Créez une nouvelle application sur https://www.dropbox.com/developers/apps
auths.tip.facebook=`Enregistrez une nouvelle application sur https://developers.facebook.com/apps et ajoutez le produit "Facebook Login"`
auths.tip.github=Créez une nouvelle application OAuth sur https://github.com/settings/applications/new
-auths.tip.gitlab=Créez une nouvelle application sur https://gitlab.com/profile/applications
auths.tip.google_plus=Obtenez des identifiants OAuth2 sur la console API de Google (https://console.developers.google.com/)
auths.tip.openid_connect=Utilisez l'URL de découvert OpenID (/.well-known/openid-configuration) pour spécifier les points d'accès
auths.tip.twitter=Rendez-vous sur https://dev.twitter.com/apps, créez une application et assurez-vous que l'option "Autoriser l'application à être utilisée avec Twitter Connect" est activée
@@ -3215,6 +3215,13 @@ notices.desc=Description
notices.op=Opération
notices.delete_success=Les informations systèmes ont été supprimées.
+self_check.no_problem_found=Aucun problème trouvé pour l’instant.
+self_check.database_collation_mismatch=Exige que la base de données utilise la collation %s.
+self_check.database_collation_case_insensitive=La base de données utilise la collation %s, insensible à la casse. Bien que Gitea soit compatible, il peut y avoir quelques rares cas qui ne fonctionnent pas comme prévu.
+self_check.database_inconsistent_collation_columns=La base de données utilise la collation %s, mais ces colonnes utilisent des collations différentes. Cela peut causer des problèmes imprévus.
+self_check.database_fix_mysql=Pour les utilisateurs de MySQL ou MariaDB, vous pouvez utiliser la commande « gitea doctor convert » dans un terminal ou exécuter une requête du type « ALTER … COLLATE ... » pour résoudre les problèmes de collation.
+self_check.database_fix_mssql=Pour les utilisateurs de MSSQL, vous ne pouvez résoudre le problème qu’en exécutant une requête SQL du type « ALTER … COLLATE … ».
+
[action]
create_repo=a créé le dépôt %s
rename_repo=a rebaptisé le dépôt %[1]s
en %[3]s
@@ -3399,6 +3406,9 @@ rpm.registry=Configurez ce registre à partir d'un terminal :
rpm.distros.redhat=sur les distributions basées sur RedHat
rpm.distros.suse=sur les distributions basées sur SUSE
rpm.install=Pour installer le paquet, exécutez la commande suivante :
+rpm.repository=Informations sur le Dépôt
+rpm.repository.architectures=Architectures
+rpm.repository.multiple_groups=Ce paquet est disponible en plusieurs groupes.
rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante :
rubygems.install2=ou ajoutez-le au Gemfile :
rubygems.dependencies.runtime=Dépendances d'exécution
@@ -3531,8 +3541,8 @@ runs.actors_no_select=Tous les acteurs
runs.status_no_select=Touts les statuts
runs.no_results=Aucun résultat correspondant.
runs.no_workflows=Il n'y a pas encore de workflows.
-runs.no_workflows.quick_start=Vous ne savez pas comment commencer avec Gitea Action ? Consultez le guide de démarrage rapide.
-runs.no_workflows.documentation=Pour plus d’informations sur les Actions Gitea, voir la documentation.
+runs.no_workflows.quick_start=Vous découvrez les Actions Gitea ? Consultez le didacticiel.
+runs.no_workflows.documentation=Pour plus d’informations sur les actions Gitea, voir la documentation.
runs.no_runs=Le flux de travail n'a pas encore d'exécution.
runs.empty_commit_message=(message de révision vide)
@@ -3551,7 +3561,7 @@ variables.none=Il n'y a pas encore de variables.
variables.deletion=Retirer la variable
variables.deletion.description=La suppression d’une variable est permanente et ne peut être défaite. Continuer ?
variables.description=Les variables sont passées aux actions et ne peuvent être lues autrement.
-variables.id_not_exist=La variable numéro %d n’existe pas.
+variables.id_not_exist=La variable avec l’ID %d n’existe pas.
variables.edit=Modifier la variable
variables.deletion.failed=Impossible de retirer la variable.
variables.deletion.success=La variable a bien été retirée.
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index aee4b44edf..fb229090d4 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -90,6 +90,14 @@ concept_user_organization=Szervezet
name=Név
+filter.is_archived=Archivált
+filter.is_template=Sablon
+filter.public=Nyilvános
+filter.private=Privát
+
+
+[search]
+
[aria]
[heatmap]
@@ -207,7 +215,6 @@ collaborative_repos=Együttműködési tárolók
my_orgs=Szervezeteim
my_mirrors=Tükreim
view_home=Nézet %s
-search_repos=Tároló keresés…
show_archived=Archivált
@@ -222,12 +229,7 @@ issues.in_your_repos=A tárolóidban
repos=Tárolók
users=Felhasználók
organizations=Szervezetek
-search=Keresés
code=Kód
-repo_no_results=Nincs ilyen tároló.
-user_no_results=Nincs ilyen felhasználó.
-org_no_results=Nincs ilyen szervezet.
-code_no_results=Nincs találat a keresési kifejezésedre.
code_last_indexed_at=Utoljára indexelve: %s
[auth]
@@ -240,7 +242,6 @@ remember_me=Eszköz megjegyzése
forgot_password_title=Elfelejtett jelszó
forgot_password=Elfelejtette a jelszavát?
sign_up_now=Szeretne bejelentkezni? Regisztráljon most.
-confirmation_mail_sent_prompt=Új megerősítő email lett küldve ide: %s. Ellenőrizze postafiókját az elkövetkező %s a regisztrációs folyamat befejezéséhez.
must_change_password=Jelszó módosítása
allow_password_change=A felhasználóknak meg kell változtatniuk a jelszavukat(ajánlott)
reset_password_mail_sent_prompt=Megerősítő email lett küldve ide: %s. Ellenőrizze postafiókját az elkövetkező %s a jelszó visszaállítási folyamat befejezéséhez.
@@ -369,6 +370,7 @@ auth_failed=A hitelesítés sikertelen: %v
target_branch_not_exist=Cél ág nem létezik.
+
[user]
change_avatar=Profilkép megváltoztatása…
repositories=Tárolók
@@ -383,6 +385,7 @@ unfollow=Követés törlése
user_bio=Életrajz
+
[settings]
profile=Profil
account=Fiók
@@ -721,8 +724,6 @@ editor.no_changes_to_show=Nincsen megjeleníthető változás.
editor.add_subdir=Mappa hozzáadása…
commits.commits=Commit-ok
-commits.search=Commit-ok keresése…
-commits.find=Keresés
commits.search_all=Minden ág
commits.author=Szerző
commits.message=Üzenet
@@ -928,7 +929,6 @@ pulls.compare_changes=Új egyesítési kérés
pulls.compare_base=egyesítés ide
pulls.compare_compare=egyesítés innen
pulls.filter_branch=Ágra szűrés
-pulls.no_results=Nincs találat.
pulls.nothing_to_compare=Ezek az ágak egyenlőek. Nincs szükség egyesítési kérésre.
pulls.create=Egyesítési kérés létrehozása
pulls.title_desc=egyesíteni szeretné %[1]d változás(oka)t a(z) %[2]s
-ból %[3]s
-ba
@@ -1053,10 +1053,7 @@ activity.git_stats_and_deletions=és
activity.git_stats_deletion_1=%d törlés
activity.git_stats_deletion_n=%d törlés
-search=Keresés
-search.search_repo=Tároló keresés
-search.results=`"%s" találatok keresése itt: %s`
-search.code_no_results=Nincs találat a keresési kifejezésedre.
+contributors.contribution_type.commits=Commit-ok
settings=Beállítások
settings.options=Tároló
@@ -1099,8 +1096,6 @@ settings.branches=Ágak
settings.protected_branch=Ág védeleme
settings.protected_branch_can_push=Push engedélyezése?
settings.protected_branch_can_push_yes=Most már push-olhatja
-settings.protect_whitelist_search_users=Felhasználó keresése…
-settings.protect_whitelist_search_teams=Csoportok keresése…
settings.protect_check_status_contexts=Állapotellenőrzés engedélyezése
settings.add_protected_branch=Védelem engedélyezése
settings.delete_protected_branch=Védelem letiltása
@@ -1168,6 +1163,8 @@ topic.done=Kész
+[graphs]
+
[org]
org_name_holder=Szervezet neve
org_full_name_holder=Szervezet teljes neve
@@ -1243,7 +1240,6 @@ teams.delete_team_desc=Egy csapat törlése visszavonja a tagjai hozzáférésé
teams.delete_team_success=A csoport törölve lett.
teams.read_permission_desc=Ez a csoport Olvasási jogosultságot biztosít: a tagok megtekinthetik és klónozhatják a csoport tárolóit.
teams.repositories=Csoport tárolói
-teams.search_repo_placeholder=Tároló keresése…
teams.remove_all_repos_title=Összes csapattároló eltávolítása
teams.remove_all_repos_desc=Ez el fogja távolítani az összes tárolót a csoportból.
teams.add_all_repos_title=Minden tároló hozzáadása
@@ -1261,6 +1257,8 @@ organizations=Szervezetek
repositories=Tárolók
authentication=Hitelesítési források
config=Konfiguráció
+config_summary=Összefoglaló
+config_settings=Beállítások
notices=Rendszer-értesítések
monitor=Figyelés
first_page=Első
@@ -1268,7 +1266,6 @@ last_page=Utolsó
total=Összesen: %d
dashboard.statistic=Összefoglaló
-dashboard.operations=Karbantartási műveletek
dashboard.system_status=Rendszer Állapota
dashboard.operation_name=Művelet Neve
dashboard.operation_switch=Váltás
@@ -1347,8 +1344,6 @@ repos.repo_manage_panel=Tárolók Kezelése
repos.owner=Tulajdonos
repos.name=Név
repos.private=Privát
-repos.watches=Figyelők
-repos.stars=Csillagok
repos.issues=Hibajegyek
repos.size=Méret
@@ -1410,7 +1405,6 @@ auths.tip.bitbucket=Igényeljen egy új OAuth jogosultságot itt: https://bitbuc
auths.tip.dropbox=Vegyen fel új alkalmazást itt: https://www.dropbox.com/developers/apps
auths.tip.facebook=Vegyen fel új alkalmazást itt: https://developers.facebook.com/apps majd adja hozzá a "Facebook Login"-t
auths.tip.github=Vegyen fel új OAuth alkalmazást itt: https://github.com/settings/applications/new
-auths.tip.gitlab=Vegyen fel új alkalmazást itt: https://gitlab.com/profile/applications
auths.tip.google_plus=Szerezzen OAuth2 kliens hitelesítési adatokat a Google API konzolban (https://console.developers.google.com/)
auths.tip.openid_connect=Használja az OpenID kapcsolódás felfedező URL-t (/.well-known/openid-configuration) a végpontok beállításához
auths.tip.twitter=Menyjen ide: https://dev.twitter.com/apps, hozzon létre egy alkalmazást és győződjön meg róla, hogy az “Allow this application to be used to Sign in with Twitter” opció be van kapcsolva
@@ -1572,6 +1566,7 @@ notices.desc=Leírás
notices.op=Op.
notices.delete_success=A rendszer-értesítések törölve lettek.
+
[action]
create_repo=létrehozott tárolót: %s
rename_repo=átnevezte a(z) %[1]s
tárolót %[3]s-ra/re
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 4dd7c299df..96248cbc1d 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -83,6 +83,12 @@ concept_code_repository=Repositori
name=Nama
+filter.is_template=Contoh
+filter.private=Pribadi
+
+
+[search]
+
[aria]
[heatmap]
@@ -134,7 +140,6 @@ collaborative_repos=Repositori Kolaboratif
my_orgs=Organisasi Saya
my_mirrors=Duplikat Saya
view_home=Lihat %s
-search_repos=Cari repositori…
show_private=Pribadi
@@ -145,12 +150,7 @@ issues.in_your_repos=Dalam repositori anda
repos=Repositori
users=Pengguna
organizations=Organisasi
-search=Cari
code=Kode
-repo_no_results=Tidak ditemukan repositori yang cocok.
-user_no_results=Tidak ditemukan pengguna yang cocok.
-org_no_results=Tidak ada organisasi yang cocok ditemukan.
-code_no_results=Tidak ada kode sumber yang cocok dengan istilah yang anda cari.
[auth]
create_new_account=Daftar Akun
@@ -161,7 +161,6 @@ disable_register_mail=Konfirmasi lewat email untuk pengguna baru dimatikan.
forgot_password_title=Lupa Kata Sandi
forgot_password=Lupa kata sandi?
sign_up_now=Butuh akun? Daftar sekarang.
-confirmation_mail_sent_prompt=Surel konfirmasi baru telah dikirim ke %s. Silakan periksa kotak masuk anda dalam %s ke depan untuk menyelesaikan proses pendaftaran.
must_change_password=Perbarui kata sandi Anda
allow_password_change=Wajibkan pengguna untuk mengganti kata sandi (disarankan)
reset_password_mail_sent_prompt=Surel konfirmasi berhasil dikirim ke %s. Silahkan cek akun email Anda dalam %s jam untuk menyelesaikan proses pemulihan akun.
@@ -293,6 +292,7 @@ auth_failed=Otentikasi gagal: %v
target_branch_not_exist=Target cabang tidak ada.
+
[user]
change_avatar=Ganti avatar anda…
repositories=Repositori
@@ -306,6 +306,7 @@ unfollow=Berhenti Mengikuti
user_bio=Biografi
+
[settings]
profile=Profil
account=Akun
@@ -630,7 +631,6 @@ editor.cancel=Membatalkan
editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan.
commits.commits=Melakukan
-commits.find=Telusuri
commits.author=Penulis
commits.message=Pesan
commits.date=Tanggal
@@ -748,7 +748,6 @@ issues.dependency.remove=Menghapus
pulls.new=Permintaan Tarik Baru
pulls.compare_changes=Permintaan Tarik Baru
pulls.filter_branch=Penyaringan cabang
-pulls.no_results=Hasil tidak ditemukan.
pulls.create=Buat Permintaan Tarik
pulls.title_desc=ingin menggabungkan komit %[1]d dari %[2]s
menuju %[3]s
pulls.merged_title_desc=commit %[1]d telah digabungkan dari %[2]s
menjadi %[3]s
%[4]s
@@ -838,10 +837,7 @@ activity.title.releases_n=%d Rilis
activity.title.releases_published_by=%s dikeluarkan oleh %s
activity.published_release_label=Dikeluarkan
-search=Cari
-search.search_repo=Cari repositori
-search.results=Cari hasil untuk "%s" dalam %s
-search.code_no_results=Tidak ada kode sumber yang cocok dengan istilah yang anda cari.
+contributors.contribution_type.commits=Melakukan
settings=Pengaturan
settings.desc=Pengaturan dimana anda dapat mengelola pengaturan untuk repositori
@@ -870,7 +866,6 @@ settings.transfer_owner=Pemilik Baru
settings.delete=Menghapus Repositori Ini
settings.delete_notices_1=- Operasi ini TIDAK BISA dibatalkan.
settings.delete_collaborator=Menghapus
-settings.search_user_placeholder=Cari pengguna…
settings.teams=Tim
settings.add_webhook=Tambahkan Webhook
settings.webhook.test_delivery=Percobaan Pengiriman
@@ -953,6 +948,8 @@ branch.deleted_by=Dihapus oleh %s
+[graphs]
+
[org]
org_name_holder=Nama Organisasi
org_full_name_holder=Organisasi Nama Lengkap
@@ -1003,13 +1000,13 @@ teams.update_settings=Memperbarui pengaturan
teams.add_team_member=Tambahkan Anggota Tim
teams.delete_team_success=Tim sudah di hapus.
teams.repositories=Tim repositori
-teams.search_repo_placeholder=Cari repositori…
[admin]
dashboard=Dasbor
organizations=Organisasi
repositories=Repositori
config=Konfigurasi
+config_settings=Pengaturan
notices=Pemberitahuan Sistem
monitor=Memantau
first_page=Pertama
@@ -1072,8 +1069,6 @@ repos.repo_manage_panel=Manajemen Repositori
repos.owner=Pemilik
repos.name=Nama
repos.private=Pribadi
-repos.watches=Jam tangan
-repos.stars=Bintang
repos.issues=Masalah
repos.size=Ukuran
@@ -1123,7 +1118,6 @@ auths.tip.oauth2_provider=Penyediaan OAuth2
auths.tip.dropbox=Membuat aplikasi baru di https://www.dropbox.com/developers/apps
auths.tip.facebook=`Daftarkan sebuah aplikasi baru di https://developers.facebook.com/apps dan tambakan produk "Facebook Masuk"`
auths.tip.github=Mendaftar aplikasi OAuth baru di https://github.com/settings/applications/new
-auths.tip.gitlab=Mendaftar aplikasi baru di https://gitlab.com/profile/applications
auths.tip.openid_connect=Gunakan membuka ID yang terhubung ke jelajah URL (/.well-known/openid-configuration) untuk menentukan titik akhir
auths.delete=Menghapus Otentikasi Sumber
auths.delete_auth_title=Menghapus Otentikasi Sumber
@@ -1262,6 +1256,7 @@ notices.desc=Deskripsi
notices.op=Op.
notices.delete_success=Laporan sistem telah dihapus.
+
[action]
create_repo=repositori dibuat %s
rename_repo=ganti nama gudang penyimpanan dari %[1]s
ke %[3]s
@@ -1336,12 +1331,53 @@ runners.task_list.repository=Repositori
runners.task_list.commit=Memperbuat
runs.commit=Memperbuat
+runs.no_matching_online_runner_helper=Tidak ada runner online yang cocok dengan label: %s
+runs.actor=Aktor
+runs.status=Status
+runs.actors_no_select=Semua aktor
+runs.status_no_select=Semua status
+runs.no_results=Tidak ada hasil yang cocok.
+runs.no_workflows=Belum ada alur kerja.
+runs.no_workflows.quick_start=Tidak tahu cara memulai dengan Gitea Actions? Lihat panduan cepat.
+runs.no_workflows.documentation=Untuk informasi lebih lanjut tentang Gitea Actions, lihat dokumentasi.
+runs.no_runs=Alur kerja belum berjalan.
+runs.empty_commit_message=(pesan commit kosong)
+workflow.disable=Nonaktifkan Alur Kerja
+workflow.disable_success=Alur kerja '%s' berhasil dinonaktifkan.
+workflow.enable=Aktifkan Alur Kerja
+workflow.enable_success=Alur kerja '%s' berhasil diaktifkan.
+workflow.disabled=Alur kerja dinonaktifkan.
+need_approval_desc=Butuh persetujuan untuk menjalankan alur kerja untuk pull request fork.
+variables=Variabel
+variables.management=Managemen Variabel
+variables.creation=Tambah Variabel
+variables.none=Belum ada variabel.
+variables.deletion=Hapus variabel
+variables.deletion.description=Menghapus variabel bersifat permanen dan tidak dapat dibatalkan. Lanjutkan?
+variables.description=Variabel akan diteruskan ke beberapa tindakan dan tidak dapat dibaca sebaliknya.
+variables.id_not_exist=Variabel dengan ID %d tidak ada.
+variables.edit=Edit Variabel
+variables.deletion.failed=Gagal menghapus variabel.
+variables.deletion.success=Variabel telah dihapus.
+variables.creation.failed=Gagal menambahkan variabel.
+variables.creation.success=Variabel "%s" telah ditambahkan.
+variables.update.failed=Gagal mengedit variabel.
+variables.update.success=Variabel telah diedit.
[projects]
+type-1.display_name=Proyek Individu
+type-2.display_name=Proyek Repositori
+type-3.display_name=Proyek Organisasi
[git.filemode]
+changed_filemode=%[1]s → %[2]s
; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Directory
+normal_file=Normal file
+executable_file=Executable file
+symbolic_link=Symbolic link
+submodule=Submodule
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index 2ba623dc12..3165c4185b 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -111,6 +111,14 @@ concept_code_repository=Hugbúnaðarsafn
name=Heiti
value=Gildi
+filter=Sía
+filter.is_archived=Safnvistað
+filter.is_template=Sniðmát
+filter.public=Opinbert
+
+
+[search]
+
[aria]
[heatmap]
@@ -224,7 +232,6 @@ show_more_repos=Sýna fleiri hugbúnaðarsöfn…
my_orgs=Stofnanir Mínar
my_mirrors=Speglanir Mínar
view_home=Skoða %s
-search_repos=Finna hugbúnaðarsafn…
filter=Aðrar Síur
show_archived=Safnvistað
@@ -239,14 +246,7 @@ issues.in_your_repos=Í hugbúnaðarsöfnum þínum
repos=Hugbúnaðarsöfn
users=Notendur
organizations=Stofnanir
-search=Leita
code=Kóði
-search.fuzzy=Óljóst
-code_search_unavailable=Sem stendur er kóðaleit ekki í boði. Vinsamlegast hafðu samband við síðustjórann þinn.
-repo_no_results=Engin samsvarandi hugbúnaðarsöfn fundust.
-user_no_results=Engir samsvarandi notendur fundust.
-org_no_results=Engar samsvarandi stofnanir fundust.
-code_no_results=Enginn samsvarandi frumkóði fannst eftur þínum leitarorðum.
[auth]
create_new_account=Skrá Notanda
@@ -401,6 +401,7 @@ team_not_exist=Liðið er ekki til.
+
[user]
change_avatar=Breyttu notandamyndinni þinni…
repositories=Hugbúnaðarsöfn
@@ -417,6 +418,7 @@ user_bio=Lífssaga
disabled_public_activity=Þessi notandi hefur slökkt á opinberum sýnileika virkninnar.
+
[settings]
profile=Notandasíða
account=Reikningur
@@ -703,7 +705,6 @@ editor.cancel=Hætta við
editor.fail_to_update_file_summary=Villuskilaboð:
commits.commits=Framlög
-commits.find=Leita
commits.author=Höfundur
commits.message=Skilaboð
commits.date=Dagsetning
@@ -727,7 +728,6 @@ projects.edit=Breyta Verkefnum
projects.modify=Uppfæra Verkefni
projects.type.none=Ekkert
projects.template.desc=Sniðmát
-projects.type.uncategorized=Óflokkuð
projects.column.edit_title=Heiti
projects.column.new_title=Heiti
projects.column.color=Litað
@@ -989,10 +989,7 @@ activity.git_stats_and_deletions=og
activity.git_stats_deletion_1=%d eyðing
activity.git_stats_deletion_n=%d eyðingar
-search=Leita
-search.fuzzy=Óljóst
-search.code_no_results=Enginn samsvarandi frumkóði fannst eftur þínum leitarorðum.
-search.code_search_unavailable=Sem stendur er kóðaleit ekki í boði. Vinsamlegast hafðu samband við síðustjórann þinn.
+contributors.contribution_type.commits=Framlög
settings=Stillingar
settings.options=Hugbúnaðarsafn
@@ -1112,6 +1109,8 @@ topic.done=Í lagi
+[graphs]
+
[org]
repo_updated=Uppfært
members=Meðlimar
@@ -1158,6 +1157,8 @@ teams.all_repositories=Öll hugbúnaðarsöfn
[admin]
repositories=Hugbúnaðarsöfn
config=Stilling
+config_summary=Yfirlit
+config_settings=Stillingar
first_page=Byrjun
last_page=Síðasta
total=Samtals: %d
@@ -1196,9 +1197,6 @@ orgs.members=Meðlimar
repos.owner=Eigandi
repos.name=Heiti
-repos.watches=Fylgist með
-repos.stars=Eftirlæti
-repos.forks=Skiptingar
repos.issues=Vandamál
repos.size=Stærð
@@ -1278,6 +1276,7 @@ notices.type_1=Hugbúnaðarsafn
notices.type_2=Verkefni
notices.desc=Lýsing
+
[action]
create_issue=`opnaði vandamál %[3]s#%[2]s`
reopen_issue=`enduropnaði vandamál %[3]s#%[2]s`
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index a30232dd10..9a22995dfb 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -116,6 +116,15 @@ concept_user_organization=Organizzazione
name=Nome
value=Valore
+filter=Filtro
+filter.is_archived=Archiviato
+filter.is_template=Template
+filter.public=Pubblico
+filter.private=Privati
+
+
+[search]
+
[aria]
[heatmap]
@@ -254,7 +263,6 @@ collaborative_repos=Repository Condivisi
my_orgs=Le mie Organizzazioni
my_mirrors=I miei Mirror
view_home=Vedi %s
-search_repos=Trova un repository…
filter=Altro filtri
filter_by_team_repositories=Filtra per repository del team
feed_of=`Feed di "%s"`
@@ -275,15 +283,7 @@ issues.in_your_repos=Nei tuoi repository
repos=Repository
users=Utenti
organizations=Organizzazioni
-search=Cerca
code=Codice
-search.fuzzy=Fuzzy
-search.match=Corrispondenze
-code_search_unavailable=Attualmente la ricerca di codice non è disponibile. Contatta l'amministratore del sito.
-repo_no_results=Nessuna repository corrispondente.
-user_no_results=Nessun utente corrispondente.
-org_no_results=Nessun'organizzazione corrispondente trovata.
-code_no_results=Nessun codice sorgente corrispondente ai termini di ricerca.
code_last_indexed_at=Ultimo indicizzato %s
[auth]
@@ -297,7 +297,6 @@ remember_me=Ricorda questo dispositivo
forgot_password_title=Password Dimenticata
forgot_password=Password dimenticata?
sign_up_now=Hai bisogno di un account? Registrati adesso.
-confirmation_mail_sent_prompt=Una nuova email di conferma è stata inviata a %s. Per favore controlla la tua posta in arrivo nelle prossime %s per completare il processo di registrazione.
must_change_password=Aggiorna la tua password
allow_password_change=Richiede all'utente di cambiare la password (scelta consigliata)
reset_password_mail_sent_prompt=Una email di conferma è stata inviata a %s. Per favore controlla la tua posta in arrivo nelle prossime %s per completare il processo di reset della password.
@@ -490,6 +489,7 @@ auth_failed=Autenticazione non riuscita: %v
target_branch_not_exist=Il ramo (branch) di destinazione non esiste.
+
[user]
change_avatar=Modifica il tuo avatar…
repositories=Repository
@@ -506,6 +506,7 @@ user_bio=Biografia
disabled_public_activity=L'utente ha disabilitato la vista pubblica dell'attività.
+
[settings]
profile=Profilo
account=Account
@@ -633,7 +634,6 @@ gpg_invalid_token_signature=La chiave GPG fornita, la firma e il token non corri
gpg_token_required=Devi fornire una firma per il token sottostante
gpg_token=Token
gpg_token_help=È possibile generare una firma utilizzando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Firma GPG corazzata
key_signature_gpg_placeholder=Comincia con '-----BEGIN PGP SIGNATURE-----'
ssh_key_verified=Chiave Verificata
@@ -786,7 +786,6 @@ already_forked=Hai già fatto il fork di %s
fork_to_different_account=Fai Fork a un account diverso
fork_visibility_helper=La visibilità di un repository forkato non può essere modificata.
use_template=Usa questo modello
-clone_in_vsc=Clona nel codice VS
download_zip=Scarica ZIP
download_tar=Scarica TAR.GZ
download_bundle=Scarica BUNDLE
@@ -1050,8 +1049,6 @@ editor.revert=Ripristina %s su:
commits.desc=Sfoglia la cronologia di modifiche del codice rogente.
commits.commits=Commit
commits.nothing_to_compare=Questi rami sono uguali.
-commits.search=Ricerca commits…
-commits.find=Cerca
commits.search_all=Tutti i branch
commits.author=Autore
commits.message=Messaggio
@@ -1096,7 +1093,6 @@ projects.type.basic_kanban=Basic Kanban
projects.type.bug_triage=Bug Triage
projects.template.desc=Template di progetto
projects.template.desc_helper=Seleziona un modello di progetto per iniziare
-projects.type.uncategorized=Senza categoria
projects.column.edit_title=Nome
projects.column.new_title=Nome
projects.column.color=Colore
@@ -1408,7 +1404,6 @@ pulls.compare_compare=esegui un pull da
pulls.switch_comparison_type=Cambia tipo di confronto
pulls.switch_head_and_base=Testa e base di commutazione
pulls.filter_branch=Filtra branch
-pulls.no_results=Nessun risultato trovato.
pulls.nothing_to_compare=Questi rami sono uguali. Non c'è alcuna necessità di creare una pull request.
pulls.nothing_to_compare_and_allow_empty_pr=Questi rami sono uguali. Questa PR sarà vuota.
pulls.has_pull_request=`Una pull request tra questi rami esiste già: %[2]s#%[3]d`
@@ -1623,13 +1618,7 @@ activity.git_stats_and_deletions=e
activity.git_stats_deletion_1=%d cancellazione
activity.git_stats_deletion_n=%d cancellazioni
-search=Ricerca
-search.search_repo=Ricerca repository
-search.fuzzy=Fuzzy
-search.match=Corrispondenze
-search.results=Risultati della ricerca per "%s" in %s
-search.code_no_results=Nessun codice sorgente corrispondente al termine di ricerca trovato.
-search.code_search_unavailable=Attualmente la ricerca di codice non è disponibile. Contatta l'amministratore del sito.
+contributors.contribution_type.commits=Commit
settings=Impostazioni
settings.desc=Impostazioni ti permette di gestire le impostazioni del repository
@@ -1757,7 +1746,6 @@ settings.delete_collaborator=Rimuovi
settings.collaborator_deletion=Rimuovi collaboratore
settings.collaborator_deletion_desc=Rimuovere un collaboratore revocherà l'accesso a questo repository. Continuare?
settings.remove_collaborator_success=Il collaboratore è stato rimosso.
-settings.search_user_placeholder=Ricerca utente…
settings.org_not_allowed_to_be_collaborator=Le organizzazioni non possono essere aggiunte come un collaboratore.
settings.change_team_access_not_allowed=La modifica dell'accesso al team per il repository è stato limitato al solo proprietario dell'organizzazione
settings.team_not_in_organization=Il team non è nella stessa organizzazione del repository
@@ -1765,7 +1753,6 @@ settings.teams=Gruppi
settings.add_team=Aggiungi Squadra
settings.add_team_duplicate=Il team ha già il repository
settings.add_team_success=Il team ha ora accesso al repository.
-settings.search_team=Cerca Squadra…
settings.change_team_permission_tip=Il permesso del team è impostato sulla pagina delle impostazioni del team e non può essere modificato per repository
settings.delete_team_tip=Questo team ha accesso a tutte le repository e non può essere rimosso
settings.remove_team_success=L'accesso del team al repository è stato rimosso.
@@ -1904,9 +1891,7 @@ settings.protect_whitelist_committers=Lista bianch push ristretti
settings.protect_whitelist_committers_desc=Solo gli utenti o i team nella whitelist potranno pushare su questo ramo (ma non forzare il push).
settings.protect_whitelist_deploy_keys=Chiavi di deploy in whitelist con permessi di scrittura per il push.
settings.protect_whitelist_users=Utenti nella whitelist per pushare:
-settings.protect_whitelist_search_users=Cerca utenti…
settings.protect_whitelist_teams=Team nella whitelist per pushare:
-settings.protect_whitelist_search_teams=Ricerca team…
settings.protect_merge_whitelist_committers=Attiva la whitelist per i merge
settings.protect_merge_whitelist_committers_desc=Consentire soltanto agli utenti o ai team in whitelist il permesso di unire le pull request di questo branch.
settings.protect_merge_whitelist_users=Utenti nella whitelist per il merging:
@@ -2117,6 +2102,8 @@ error.csv.too_large=Impossibile visualizzare questo file perché è troppo grand
error.csv.unexpected=Impossibile visualizzare questo file perché contiene un carattere inatteso alla riga %d e alla colonna %d.
error.csv.invalid_field_count=Impossibile visualizzare questo file perché ha un numero errato di campi alla riga %d.
+[graphs]
+
[org]
org_name_holder=Nome dell'Organizzazione
org_full_name_holder=Nome completo dell'organizzazione
@@ -2213,7 +2200,6 @@ teams.write_permission_desc=Questo team concede l'accesso di Scrittura
teams.admin_permission_desc=Questo team concede l'accesso di Amministratore: i membri possono leggere da, pushare su e aggiungere collaboratori ai repository del team.
teams.create_repo_permission_desc=Inoltre, questo team concede il permesso di Creare repository: i membri possono creare nuove repository nell'organizzazione.
teams.repositories=Repository di Squadra
-teams.search_repo_placeholder=Ricerca repository…
teams.remove_all_repos_title=Rimuovi tutti i repository del team
teams.remove_all_repos_desc=Questo rimuoverà tutte le repository dal team.
teams.add_all_repos_title=Aggiungi tutti i repository
@@ -2238,6 +2224,8 @@ hooks=Webhooks
authentication=Fonti di autenticazione
emails=Email Utente
config=Configurazione
+config_summary=Riepilogo
+config_settings=Impostazioni
notices=Avvisi di sistema
monitor=Monitoraggio
first_page=Prima
@@ -2245,7 +2233,6 @@ last_page=Ultima
total=Totale: %d
dashboard.statistic=Riepilogo
-dashboard.operations=Operazioni di manutenzione
dashboard.system_status=Stato del sistema
dashboard.operation_name=Nome Operazione
dashboard.operation_switch=Cambia
@@ -2392,9 +2379,6 @@ repos.unadopted.no_more=Nessun repository non adottato trovato
repos.owner=Proprietario
repos.name=Nome
repos.private=Privati
-repos.watches=Segue
-repos.stars=Voti
-repos.forks=Fork
repos.issues=Problemi
repos.size=Dimensione
@@ -2510,7 +2494,6 @@ auths.tip.nextcloud=`Registra un nuovo OAuth sulla tua istanza utilizzando il se
auths.tip.dropbox=Crea una nuova applicazione su https://www.dropbox.com/developers/apps
auths.tip.facebook=`Registra una nuova applicazione su https://developers.facebook.com/apps e aggiungi il prodotto "Facebook Login"`
auths.tip.github=Registra una nuova applicazione OAuth su https://github.com/settings/applications/new
-auths.tip.gitlab=Registra una nuova applicazione su https://gitlab.com/profile/applications
auths.tip.google_plus=Ottieni le credenziali del client OAuth2 dalla console API di Google su https://console.developers.google.com/
auths.tip.openid_connect=Utilizza l'OpenID Connect Discovery URL (/.well-known/openid-configuration) per specificare gli endpoint
auths.tip.twitter=Vai su https://dev.twitter.com/apps, crea una applicazione e assicurati che l'opzione "Allow this application to be used to Sign In with Twitter" sia abilitata
@@ -2703,6 +2686,7 @@ notices.desc=Descrizione
notices.op=Op.
notices.delete_success=Gli avvisi di sistema sono stati eliminati.
+
[action]
create_repo=ha creato il repository %s
rename_repo=repository rinominato da %[1]s
a [3]s
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 9216277955..0edd6c5dd7 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -113,6 +113,7 @@ loading=読み込み中…
error=エラー
error404=アクセスしようとしたページは存在しないか、閲覧が許可されていません。
go_back=戻る
+invalid_data=無効なデータ: %v
never=無し
unknown=不明
@@ -123,6 +124,7 @@ pin=ピン留め
unpin=ピン留め解除
artifacts=成果物
+confirm_delete_artifact=アーティファクト %s を削除してよろしいですか?
archived=アーカイブ
@@ -141,6 +143,43 @@ confirm_delete_selected=選択したすべてのアイテムを削除してよ
name=名称
value=値
+filter=フィルター
+filter.clear=フィルターをクリア
+filter.is_archived=アーカイブ
+filter.not_archived=非アーカイブ
+filter.is_fork=フォーク
+filter.not_fork=非フォーク
+filter.is_mirror=ミラー
+filter.not_mirror=非ミラー
+filter.is_template=テンプレート
+filter.not_template=非テンプレート
+filter.public=公開
+filter.private=プライベート
+
+no_results_found=見つかりません。
+
+[search]
+search=検索…
+type_tooltip=検索タイプ
+fuzzy=あいまい
+fuzzy_tooltip=検索ワードに近い結果も含めます
+match=一致
+match_tooltip=検索ワードと完全に一致する結果のみ含めます
+repo_kind=リポジトリを検索...
+user_kind=ユーザーを検索...
+org_kind=組織を検索...
+team_kind=チームを検索…
+code_kind=コードを検索...
+code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
+code_search_by_git_grep=現在のコード検索結果は "git grep" で提供されています。 サイト管理者がリポジトリインデクサーを有効にすると、より良い結果が得られるかもしれません。
+package_kind=パッケージを検索...
+project_kind=プロジェクトを検索...
+branch_kind=ブランチを検索...
+commit_kind=コミットを検索...
+runner_kind=ランナーを検索...
+no_results=一致する結果が見つかりませんでした
+keyword_search_unavailable=現在キーワード検索は利用できません。 サイト管理者にお問い合わせください。
+
[aria]
navbar=ナビゲーションバー
footer=フッター
@@ -148,8 +187,8 @@ footer.software=ソフトウェアについて
footer.links=リンク
[heatmap]
-number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 個の貢献
-no_contributions=貢献なし
+number_of_contributions_in_the_last_12_months=過去 12 か月間で %s 件の実績
+no_contributions=実績なし
less=少
more=多
@@ -246,6 +285,7 @@ email_title=メール設定
smtp_addr=SMTPホスト
smtp_port=SMTPポート
smtp_from=メール送信者
+smtp_from_invalid=「メール送信者」のアドレスが無効です
smtp_from_helper=Giteaが使用するメールアドレス。 メールアドレスのみ、または、 "名前" の形式で入力してください。
mailer_user=SMTPユーザー名
mailer_password=SMTPパスワード
@@ -305,6 +345,7 @@ env_config_keys=環境設定
env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます:
[home]
+nav_menu=ナビゲーションメニュー
uname_holder=ユーザー名またはメールアドレス
password_holder=パスワード
switch_dashboard_context=ダッシュボードのコンテキスト切替
@@ -314,7 +355,6 @@ collaborative_repos=共同リポジトリ
my_orgs=自分の組織
my_mirrors=自分のミラー
view_home=%s を表示
-search_repos=リポジトリを探す…
filter=その他のフィルター
filter_by_team_repositories=チームリポジトリで絞り込み
feed_of=`"%s" のフィード`
@@ -335,20 +375,8 @@ issues.in_your_repos=あなたのリポジトリ
repos=リポジトリ
users=ユーザー
organizations=組織
-search=検索
go_to=開く
code=コード
-search.type.tooltip=検索タイプ
-search.fuzzy=あいまい
-search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます
-search.match=一致
-search.match.tooltip=検索ワードに一致する結果だけを含めます
-code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
-repo_no_results=一致するリポジトリが見つかりません。
-user_no_results=一致するユーザーが見つかりません。
-org_no_results=一致する組織が見つかりません。
-code_no_results=検索ワードに一致するソースコードが見つかりません。
-code_search_results=`"%s" の検索結果`
code_last_indexed_at=最終取得 %s
relevant_repositories_tooltip=フォークリポジトリや、トピック、アイコン、説明のいずれも無いリポジトリは表示されません。
relevant_repositories=妥当と思われるリポジトリのみを表示しています。 フィルタリングしない結果を表示。
@@ -366,7 +394,6 @@ forgot_password_title=パスワードを忘れた
forgot_password=パスワードをお忘れですか?
sign_up_now=アカウントが必要ですか? 今すぐ登録しましょう。
sign_up_successful=アカウントは無事に作成されました。ようこそ!
-confirmation_mail_sent_prompt=%s に確認メールを送信しました。 %s以内に受信トレイを確認し、登録手続きを完了してください。
must_change_password=パスワードの更新
allow_password_change=ユーザーはパスワードの変更が必要 (推奨)
reset_password_mail_sent_prompt=%s に確認メールを送信しました。 %s以内に受信トレイを確認し、アカウント回復手続きを完了してください。
@@ -423,6 +450,7 @@ authorization_failed_desc=無効なリクエストを検出したため認可が
sspi_auth_failed=SSPI認証に失敗しました
password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した盗まれたパスワードのリストに含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。
password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした
+last_admin=最後の管理者は削除できません。少なくとも一人の管理者が必要です。
[mail]
view_it_on=%s で見る
@@ -588,6 +616,8 @@ org_still_own_packages=組織はまだ1つ以上のパッケージを所有し
target_branch_not_exist=ターゲットのブランチが存在していません。
+admin_cannot_delete_self=あなたが管理者である場合、自分自身を削除することはできません。最初に管理者権限を削除してください。
+
[user]
change_avatar=アバターを変更…
joined_on=%sに登録
@@ -613,6 +643,30 @@ form.name_reserved=ユーザー名 "%s" は予約されています。
form.name_pattern_not_allowed=`"%s" の形式はユーザー名に使用できません。`
form.name_chars_not_allowed=ユーザー名 "%s" には無効な文字が含まれています。
+block.block=ブロック
+block.block.user=ユーザーをブロック
+block.block.org=組織向けにユーザーをブロック
+block.block.failure=ユーザーのブロックに失敗しました: %s
+block.unblock=ブロックを解除
+block.unblock.failure=ユーザーのブロック解除に失敗しました: %s
+block.blocked=あなたはこのユーザーをブロックしています。
+block.title=ユーザーをブロックする
+block.info=ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。 ユーザーのブロックについてはよく確認してください。
+block.info_1=ユーザーをブロックすることで、あなたのアカウントとあなたのリポジトリに対する以下の行為を阻止します:
+block.info_2=あなたのアカウントのフォロー
+block.info_3=あなたのユーザー名で@メンションして通知を送ること
+block.info_4=そのユーザーのリポジトリに、あなたを共同作業者として招待すること
+block.info_5=リポジトリへの、スター、フォーク、ウォッチ
+block.info_6=イシューやプルリクエストの作成、コメント投稿
+block.info_7=イシューやプルリクエストでの、あなたのコメントに対するリアクションの送信
+block.user_to_block=ブロックするユーザー
+block.note=メモ
+block.note.title=メモ(任意):
+block.note.info=メモはブロックされるユーザーには表示されません。
+block.note.edit=メモを編集
+block.list=ブロックしたユーザー
+block.list.none=ブロックしているユーザーはいません。
+
[settings]
profile=プロフィール
account=アカウント
@@ -655,8 +709,8 @@ language=言語
ui=テーマ
hidden_comment_types=非表示にするコメントの種類
hidden_comment_types_description=ここでチェックを入れたコメントの種類は、イシューのページには表示されません。 たとえば「ラベル」にチェックを入れると、「<ユーザー> が <ラベル> を追加/削除」といったコメントはすべて除去されます。
-hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照されたというコメント
-hidden_comment_types.issue_ref_tooltip=このイシューに関連付けるブランチやタグをユーザーが変更したというコメント
+hidden_comment_types.ref_tooltip=このイシューが別のイシューやコミット等から参照された、というコメント
+hidden_comment_types.issue_ref_tooltip=このイシューのブランチやタグへの関連付けをユーザーが変更した、というコメント
comment_type_group_reference=参照
comment_type_group_label=ラベル
comment_type_group_milestone=マイルストーン
@@ -726,7 +780,7 @@ add_email_success=新しいメールアドレスを追加しました。
email_preference_set_success=メール設定を保存しました。
add_openid_success=新しいOpenIDアドレスを追加しました。
keep_email_private=メールアドレスを隠す
-keep_email_private_popup=これによりプロフィールでメールアドレスが隠され、Webインターフェースでのプルリクエスト作成やファイル編集でもメールアドレスが隠されます。 プッシュ済みのコミットは変更されません。
+keep_email_private_popup=あなたのプロフィールからメールアドレスが隠され、Webインターフェースを使ったプルリクエスト作成やファイル編集でも、メールアドレスが隠されます。 プッシュ済みのコミットは変更されません。 コミットであなたのアカウントに関連付ける場合は %s を使用してください。
openid_desc=OpenIDを使うと外部プロバイダーに認証を委任することができます。
manage_ssh_keys=SSHキーの管理
@@ -757,7 +811,6 @@ gpg_invalid_token_signature=入力されたGPG鍵、署名、トークンが合
gpg_token_required=以下のトークンの署名を入力する必要があります
gpg_token=トークン
gpg_token_help=署名はこの方法で生成できます:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Armor形式のGPG署名
key_signature_gpg_placeholder=先頭は '-----BEGIN PGP SIGNATURE-----'
verify_gpg_key_success=GPG鍵 "%s" を確認しました。
@@ -951,7 +1004,7 @@ fork_branch=フォークにクローンされるブランチ
all_branches=すべてのブランチ
fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。
use_template=このテンプレートを使用
-clone_in_vsc=VSCodeでクローン
+open_with_editor=%s で開く
download_zip=ZIPファイルをダウンロード
download_tar=TAR.GZファイルをダウンロード
download_bundle=バンドルをダウンロード
@@ -967,6 +1020,8 @@ issue_labels_helper=イシューのラベルセットを選択
license=ライセンス
license_helper=ライセンス ファイルを選択してください。
license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? ライセンス選択サイト も確認してみてください。
+object_format=オブジェクトのフォーマット
+object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。
readme=README
readme_helper=READMEファイル テンプレートを選択してください。
readme_helper_desc=プロジェクトについての説明をひととおり書く場所です。
@@ -984,6 +1039,7 @@ mirror_prune=Prune
mirror_prune_desc=不要になった古いリモートトラッキング参照を削除
mirror_interval=ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 定期的な同期を無効にする場合は0。(最小間隔: %s)
mirror_interval_invalid=ミラー間隔が不正です。
+mirror_sync=前回の同期
mirror_sync_on_commit=コミットがプッシュされたときに同期
mirror_address=クローンするURL
mirror_address_desc=必要な資格情報は「認証」セクションに設定してください。
@@ -1001,6 +1057,7 @@ watchers=ウォッチャー
stargazers=スターゲイザー
stars_remove_warning=これを指定すると、このリポジトリのスターはすべて削除されます。
forks=フォーク
+stars=スター
reactions_more=さらに %d 件
unit_disabled=サイト管理者がこのリポジトリセクションを無効にしています。
language_other=その他
@@ -1032,8 +1089,9 @@ transfer.no_permission_to_reject=この移転を拒否する権限がありま
desc.private=プライベート
desc.public=公開
desc.template=テンプレート
-desc.internal=組織内
+desc.internal=内部
desc.archived=アーカイブ
+desc.sha256=SHA256
template.items=テンプレート項目
template.git_content=Gitコンテンツ (デフォルトブランチ)
@@ -1184,6 +1242,8 @@ audio_not_supported_in_browser=このブラウザーはHTML5のaudioタグをサ
stored_lfs=Git LFSで保管されています
symbolic_link=シンボリック リンク
executable_file=実行ファイル
+vendored=ベンダーファイル
+generated=生成ファイル
commit_graph=コミットグラフ
commit_graph.select=ブランチを選択
commit_graph.hide_pr_refs=プルリクエストを非表示
@@ -1247,6 +1307,8 @@ editor.file_editing_no_longer_exists=編集中のファイル "%s" が、もう
editor.file_deleting_no_longer_exists=削除しようとしたファイル "%s" が、すでにリポジトリ内にありません。
editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 ここをクリックして何が変更されたか確認するか、もう一度"変更をコミット"をクリックして上書きします。
editor.file_already_exists=ファイル "%s" は、このリポジトリに既に存在します。
+editor.commit_id_not_matching=コミットIDが編集を開始したときのIDと一致しません。 パッチ用のブランチにコミットしたあとマージしてください。
+editor.push_out_of_date=このプッシュは最新ではないようです。
editor.commit_empty_file_header=空ファイルのコミット
editor.commit_empty_file_text=コミットしようとしているファイルは空です。 続行しますか?
editor.no_changes_to_show=表示する変更箇所はありません。
@@ -1270,9 +1332,8 @@ commits.desc=ソースコードの変更履歴を参照します。
commits.commits=コミット
commits.no_commits=共通のコミットはありません。 "%s" と "%s" の履歴はすべて異なっています。
commits.nothing_to_compare=二つのブランチは同じ内容です。
-commits.search=コミットの検索…
commits.search.tooltip=`キーワード "author:"、"committer:"、"after:"、"before:" を付けて指定できます。 例 "revert author:Alice before:2019-01-13"`
-commits.find=検索
+commits.search_branch=このブランチ
commits.search_all=すべてのブランチ
commits.author=作成者
commits.message=メッセージ
@@ -1323,7 +1384,6 @@ projects.type.basic_kanban=基本的なカンバン
projects.type.bug_triage=バグ トリアージ
projects.template.desc=テンプレート
projects.template.desc_helper=開始するプロジェクトテンプレートを選択
-projects.type.uncategorized=未分類
projects.column.edit=列を編集
projects.column.edit_title=名称
projects.column.new_title=名称
@@ -1331,10 +1391,8 @@ projects.column.new_submit=列を作成
projects.column.new=新しい列
projects.column.set_default=デフォルトに設定
projects.column.set_default_desc=この列を未分類のイシューやプルリクエストが入るデフォルトの列にします
-projects.column.unset_default=デフォルトを解除
-projects.column.unset_default_desc=この列からデフォルト列の設定を解除します
projects.column.delete=列を削除
-projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューが '未分類' に移動します。 続行しますか?
+projects.column.deletion_desc=プロジェクト列を削除すると、関連するすべてのイシューがデフォルトの列に移動します。 続行しますか?
projects.column.color=カラー
projects.open=オープン
projects.close=クローズ
@@ -1446,7 +1504,6 @@ issues.filter_sort.moststars=スターが多い順
issues.filter_sort.feweststars=スターが少ない順
issues.filter_sort.mostforks=フォークが多い順
issues.filter_sort.fewestforks=フォークが少ない順
-issues.keyword_search_unavailable=現在キーワード検索は利用できません。 サイト管理者にお問い合わせください。
issues.action_open=オープン
issues.action_close=クローズ
issues.action_label=ラベル
@@ -1503,7 +1560,7 @@ issues.role.member_helper=このユーザーはこのリポジトリを所有し
issues.role.collaborator=共同作業者
issues.role.collaborator_helper=このユーザーはリポジトリ上で共同作業するように招待されています。
issues.role.first_time_contributor=初めての貢献者
-issues.role.first_time_contributor_helper=これは、このユーザーのリポジトリへの最初の貢献です。
+issues.role.first_time_contributor_helper=これは、このユーザーによるリポジトリへの最初の貢献です。
issues.role.contributor=貢献者
issues.role.contributor_helper=このユーザーは以前にリポジトリにコミットしています。
issues.re_request_review=レビューを再依頼
@@ -1698,7 +1755,6 @@ pulls.compare_compare=プル元
pulls.switch_comparison_type=比較の種類を切り替える
pulls.switch_head_and_base=ヘッドとベースを切り替える
pulls.filter_branch=ブランチの絞り込み
-pulls.no_results=結果が見つかりませんでした。
pulls.show_all_commits=すべてのコミットを表示
pulls.show_changes_since_your_last_review=前回の自分のレビューからの変更を表示
pulls.showing_only_single_commit=コミット %[1]s の変更だけを表示しています
@@ -1707,6 +1763,7 @@ pulls.select_commit_hold_shift_for_range=コミットを選択。シフトを押
pulls.review_only_possible_for_full_diff=すべての差分を表示しているときだけレビューが可能です
pulls.filter_changes_by_commit=コミットで絞り込み
pulls.nothing_to_compare=同じブランチ同士のため、 プルリクエストを作成する必要がありません。
+pulls.nothing_to_compare_have_tag=選択したブランチ/タグは同一のものです。
pulls.nothing_to_compare_and_allow_empty_pr=これらのブランチは内容が同じです。 空のプルリクエストになります。
pulls.has_pull_request=`同じブランチのプルリクエストはすでに存在します: %[2]s#%[3]d`
pulls.create=プルリクエストを作成
@@ -1735,7 +1792,7 @@ pulls.is_checking=マージのコンフリクトを確認中です。 少し待
pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。
pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。
pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。
-pulls.required_status_check_missing=必要なステータスチェックが見つかりません。
+pulls.required_status_check_missing=必要なチェックがいくつか抜けています。
pulls.required_status_check_administrator=管理者であるため、このプルリクエストをマージすることは可能です。
pulls.blocked_by_approvals=このプルリクエストはまだ承認数が足りません。 %[1]d/%[2]dの承認を得ています。
pulls.blocked_by_rejection=このプルリクエストは公式レビューアにより変更要請されています。
@@ -1765,6 +1822,7 @@ pulls.merge_pull_request=マージコミットを作成
pulls.rebase_merge_pull_request=リベース後にファストフォワード
pulls.rebase_merge_commit_pull_request=リベース後にマージコミット作成
pulls.squash_merge_pull_request=スカッシュコミットを作成
+pulls.fast_forward_only_merge_pull_request=ファストフォワードのみ
pulls.merge_manually=手動マージ済みにする
pulls.merge_commit_id=マージコミットID
pulls.require_signed_wont_sign=ブランチでは署名されたコミットが必須ですが、このマージでは署名がされません
@@ -1901,6 +1959,9 @@ wiki.page_name_desc=この Wiki ページの名前を入力してください。
wiki.original_git_entry_tooltip=フレンドリーリンクを使用する代わりにオリジナルのGitファイルを表示します。
activity=アクティビティ
+activity.navbar.pulse=Pulse
+activity.navbar.contributors=貢献者
+activity.navbar.recent_commits=最近のコミット
activity.period.filter_label=期間:
activity.period.daily=1日
activity.period.halfweekly=3日
@@ -1966,16 +2027,10 @@ activity.git_stats_and_deletions=、
activity.git_stats_deletion_1=%d行削除
activity.git_stats_deletion_n=%d行削除
-search=検索
-search.search_repo=リポジトリを検索
-search.type.tooltip=検索タイプ
-search.fuzzy=あいまい
-search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます
-search.match=一致
-search.match.tooltip=検索ワードに一致する結果だけを含めます
-search.results=%[3]s 内での "%[1]s" の検索結果
-search.code_no_results=検索ワードに一致するソースコードが見つかりません。
-search.code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。
+contributors.contribution_type.filter_label=実績タイプ:
+contributors.contribution_type.commits=コミット
+contributors.contribution_type.additions=追加
+contributors.contribution_type.deletions=削除
settings=設定
settings.desc=設定では、リポジトリの設定を管理することができます。
@@ -2002,7 +2057,8 @@ settings.mirror_settings.docs.more_information_if_disabled=プッシュミラー
settings.mirror_settings.docs.doc_link_title=リポジトリをミラーリングするには?
settings.mirror_settings.docs.doc_link_pull_section=ドキュメントの「リモートリポジトリからのプル」セクション。
settings.mirror_settings.docs.pulling_remote_title=リモートリポジトリからのプル
-settings.mirror_settings.mirrored_repository=同期するリポジトリ
+settings.mirror_settings.mirrored_repository=ミラー元のリポジトリ
+settings.mirror_settings.pushed_repository=プッシュ先のリポジトリ
settings.mirror_settings.direction=方向
settings.mirror_settings.direction.pull=プル
settings.mirror_settings.direction.push=プッシュ
@@ -2053,7 +2109,8 @@ settings.pulls.default_delete_branch_after_merge=デフォルトでプルリク
settings.pulls.default_allow_edits_from_maintainers=デフォルトでメンテナからの編集を許可する
settings.releases_desc=リリースを有効にする
settings.packages_desc=リポジトリパッケージレジストリを有効にする
-settings.projects_desc=リポジトリプロジェクトを有効にする
+settings.projects_desc=プロジェクトを有効にする
+settings.projects_mode_all=すべてのプロジェクト
settings.actions_desc=Actionsを有効にする
settings.admin_settings=管理者用設定
settings.admin_enable_health_check=リポジトリのヘルスチェックを有効にする (git fsck)
@@ -2128,7 +2185,6 @@ settings.delete_collaborator=削除
settings.collaborator_deletion=共同作業者の削除
settings.collaborator_deletion_desc=共同作業者を削除し、このリポジトリへのアクセス権を取り消します。 続行しますか?
settings.remove_collaborator_success=共同作業者を削除しました。
-settings.search_user_placeholder=ユーザーを検索…
settings.org_not_allowed_to_be_collaborator=組織を共同作業者として追加することはできません。
settings.change_team_access_not_allowed=リポジトリに対するチームアクセス権の変更は、組織のオーナーのみに制限されています。
settings.team_not_in_organization=チームがリポジトリと同じ組織に属していません。
@@ -2136,7 +2192,6 @@ settings.teams=チーム
settings.add_team=チームを追加
settings.add_team_duplicate=チームにはすでにこのリポジトリが登録されています。
settings.add_team_success=チームがこのリポジトリにアクセスできるようになりました。
-settings.search_team=チームを検索…
settings.change_team_permission_tip=チームの権限はチーム設定ページで設定されており、リポジトリごとに変更することはできません
settings.delete_team_tip=このチームはすべてのリポジトリにアクセスでき、削除できません
settings.remove_team_success=チームのこのリポジトリへのアクセス権を削除しました。
@@ -2289,9 +2344,7 @@ settings.protect_whitelist_committers=ホワイトリストでプッシュを制
settings.protect_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにのみ、このブランチへのプッシュが許可されます。(強制プッシュ以外)
settings.protect_whitelist_deploy_keys=プッシュ可能な書き込み権限を持つデプロイキーをホワイトリストに含める。
settings.protect_whitelist_users=プッシュ・ホワイトリストに含むユーザー:
-settings.protect_whitelist_search_users=ユーザーを検索…
settings.protect_whitelist_teams=プッシュ・ホワイトリストに含むチーム:
-settings.protect_whitelist_search_teams=チームを検索…
settings.protect_merge_whitelist_committers=マージ・ホワイトリストを有効にする
settings.protect_merge_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにだけ、このブランチに対するプルリクエストのマージを許可します。
settings.protect_merge_whitelist_users=マージ・ホワイトリストに含むユーザー:
@@ -2312,6 +2365,8 @@ settings.protect_approvals_whitelist_users=ホワイトリストに含めるレ
settings.protect_approvals_whitelist_teams=ホワイトリストに含めるレビューチーム:
settings.dismiss_stale_approvals=古くなった承認を取り消す
settings.dismiss_stale_approvals_desc=プルリクエストの内容を変える新たなコミットがブランチにプッシュされた場合、以前の承認を取り消します。
+settings.ignore_stale_approvals=古くなった承認を無視する
+settings.ignore_stale_approvals_desc=古いコミットに対して行われた承認 (古いレビュー) を、PRの承認数にカウントしません。 古いレビューが取り消される場合は関係ありません。
settings.require_signed_commits=コミット署名必須
settings.require_signed_commits_desc=署名されていない場合、または署名が検証できなかった場合は、このブランチへのプッシュを拒否します。
settings.protect_branch_name_pattern=保護ブランチ名のパターン
@@ -2367,6 +2422,7 @@ settings.archive.error=リポジトリのアーカイブ設定でエラーが発
settings.archive.error_ismirror=ミラーのリポジトリはアーカイブできません。
settings.archive.branchsettings_unavailable=ブランチ設定は、アーカイブリポジトリでは使用できません。
settings.archive.tagsettings_unavailable=タグ設定は、アーカイブリポジトリでは使用できません。
+settings.archive.mirrors_unavailable=リポジトリがアーカイブされている場合、ミラーは利用できません。
settings.unarchive.button=アーカイブ解除
settings.unarchive.header=このリポジトリをアーカイブ解除
settings.unarchive.text=リポジトリのアーカイブを解除すると、コミット、プッシュ、新規のイシューやプルリクエストを受け付ける機能が復活します。
@@ -2533,7 +2589,6 @@ branch.default_deletion_failed=ブランチ "%s" はデフォルトブランチ
branch.restore=ブランチ "%s" の復元
branch.download=ブランチ "%s" をダウンロード
branch.rename=ブランチ名 "%s" を変更
-branch.search=ブランチを検索
branch.included_desc=このブランチはデフォルトブランチに含まれています
branch.included=埋没
branch.create_new_branch=このブランチをもとに作成します:
@@ -2565,6 +2620,14 @@ error.csv.too_large=このファイルは大きすぎるため表示できませ
error.csv.unexpected=このファイルは %d 行目の %d 文字目に予期しない文字が含まれているため表示できません。
error.csv.invalid_field_count=このファイルは %d 行目のフィールドの数が正しくないため表示できません。
+[graphs]
+component_loading=%sを読み込み中...
+component_loading_failed=%sを読み込めませんでした
+component_loading_info=少し時間がかかるかもしれません…
+component_failed_to_load=予期しないエラーが発生しました。
+contributors.what=実績
+recent_commits.what=最近のコミット
+
[org]
org_name_holder=組織名
org_full_name_holder=組織のフルネーム
@@ -2669,7 +2732,6 @@ teams.write_permission_desc=このチームは書き込みア
teams.admin_permission_desc=このチームは管理者アクセス権を持ちます: メンバーはチームリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。
teams.create_repo_permission_desc=さらに、このチームにはリポジトリの作成権限が与えられています: メンバーは組織のリポジトリを新たに作成できます。
teams.repositories=チームのリポジトリ
-teams.search_repo_placeholder=リポジトリを検索…
teams.remove_all_repos_title=チームリポジトリをすべて除去
teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。
teams.add_all_repos_title=すべてのリポジトリを追加
@@ -2678,6 +2740,7 @@ teams.add_nonexistent_repo=追加しようとしているリポジトリは存
teams.add_duplicate_users=ユーザーは既にチームのメンバーです。
teams.repos.none=このチームがアクセスできるリポジトリはありません。
teams.members.none=このチームにはメンバーがいません。
+teams.members.blocked_user=組織によってブロックされているため、ユーザーを追加できません。
teams.specific_repositories=指定したリポジトリ
teams.specific_repositories_helper=メンバーは、明示的にチームへ追加したリポジトリにのみアクセスできます。 これを選択しても、すでにすべてのリポジトリで追加されたリポジトリは自動的に除去されません。
teams.all_repositories=すべてのリポジトリ
@@ -2691,6 +2754,7 @@ teams.invite.description=下のボタンをクリックしてチームに参加
[admin]
dashboard=ダッシュボード
+self_check=セルフチェック
identity_access=アイデンティティとアクセス
users=ユーザーアカウント
organizations=組織
@@ -2701,6 +2765,8 @@ integrations=連携
authentication=認証ソース
emails=ユーザーメールアドレス
config=設定
+config_summary=サマリー
+config_settings=設定
notices=システム通知
monitor=モニタリング
first_page=最初
@@ -2710,7 +2776,6 @@ settings=管理設定
dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は ブログ を確認してください。
dashboard.statistic=サマリー
-dashboard.operations=メンテナンス操作
dashboard.system_status=システム状況
dashboard.operation_name=操作の名称
dashboard.operation_switch=切り替え
@@ -2736,6 +2801,7 @@ dashboard.delete_missing_repos=Gitファイルが存在しないリポジトリ
dashboard.delete_missing_repos.started=Gitファイルが存在しないリポジトリをすべて削除するタスクを開始しました。
dashboard.delete_generated_repository_avatars=自動生成したリポジトリアバターを削除
dashboard.sync_repo_branches=Gitデータからデータベースへ不足しているブランチを同期
+dashboard.sync_repo_tags=Gitデータからデータベースへタグを同期
dashboard.update_mirrors=ミラーの更新
dashboard.repo_health_check=全リポジトリのヘルスチェック
dashboard.check_repo_stats=全リポジトリの統計情報を更新
@@ -2790,6 +2856,7 @@ dashboard.stop_endless_tasks=終わらないタスクを停止
dashboard.cancel_abandoned_jobs=放置されたままのジョブをキャンセル
dashboard.start_schedule_tasks=スケジュールタスクを開始
dashboard.sync_branch.started=ブランチの同期を開始しました
+dashboard.sync_tag.started=タグの同期を開始しました
dashboard.rebuild_issue_indexer=イシューインデクサーの再構築
users.user_manage_panel=ユーザーアカウント管理
@@ -2875,9 +2942,6 @@ repos.unadopted.no_more=未登録のリポジトリはありません
repos.owner=オーナー
repos.name=名称
repos.private=プライベート
-repos.watches=ウォッチ
-repos.stars=スター
-repos.forks=フォーク
repos.issues=イシュー
repos.size=サイズ
repos.lfs_size=LFSサイズ
@@ -2897,12 +2961,12 @@ packages.size=サイズ
packages.published=配布
defaulthooks=デフォルトWebhook
-defaulthooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義されたWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくはWebhooksガイドをご覧下さい。
+defaulthooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくはWebhooksガイドをご覧下さい。
defaulthooks.add_webhook=デフォルトWebhookの追加
defaulthooks.update_webhook=デフォルトWebhookの更新
systemhooks=システムWebhook
-systemhooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義したWebhookはシステム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくはWebhooksガイドをご覧下さい。
+systemhooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookは、システム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくはWebhooksガイドをご覧下さい。
systemhooks.add_webhook=システムWebhookを追加
systemhooks.update_webhook=システムWebhookを更新
@@ -3002,7 +3066,7 @@ auths.tip.nextcloud=新しいOAuthコンシューマーを、インスタンス
auths.tip.dropbox=新しいアプリケーションを https://www.dropbox.com/developers/apps から登録してください。
auths.tip.facebook=新しいアプリケーションを https://developers.facebook.com/apps で登録し、"Facebook Login"を追加してください。
auths.tip.github=新しいOAuthアプリケーションを https://github.com/settings/applications/new から登録してください。
-auths.tip.gitlab=新しいアプリケーションを https://gitlab.com/profile/applications から登録してください。
+auths.tip.gitlab_new=新しいアプリケーションを https://gitlab.com/-/profile/applications から登録してください。
auths.tip.google_plus=OAuth2クライアント資格情報を、Google APIコンソール https://console.developers.google.com/ から取得してください。
auths.tip.openid_connect=OpenID Connect DiscoveryのURL (/.well-known/openid-configuration) をエンドポイントとして指定してください
auths.tip.twitter=https://dev.twitter.com/apps へアクセスしてアプリケーションを作成し、“Allow this application to be used to Sign in with Twitter”オプションを有効にしてください。
@@ -3138,6 +3202,7 @@ config.picture_config=画像とアバターの設定
config.picture_service=画像サービス
config.disable_gravatar=Gravatarが無効
config.enable_federated_avatar=フェデレーテッド・アバター有効
+config.open_with_editor_app_help=クローンメニューの「~で開く」に表示するエディタ。 空白のままにするとデフォルトが使用されます。 展開するとデフォルトを確認できます。
config.git_config=Git設定
config.git_disable_diff_highlight=Diffのシンタックスハイライトが無効
@@ -3216,6 +3281,13 @@ notices.desc=説明
notices.op=操作
notices.delete_success=システム通知を削除しました。
+self_check.no_problem_found=今のところ問題は見つかっていません。
+self_check.database_collation_mismatch=データベースに想定される照合順序: %s
+self_check.database_collation_case_insensitive=データベースは照合順序 %s を使用しており、大文字小文字を区別しません。 Giteaはその照合順序でも動作するかもしれませんが、まれに期待どおり動作しないケースがあるかもしれません。
+self_check.database_inconsistent_collation_columns=データベースは照合順序 %s を使用していますが、以下のカラムはそれと一致しない照合順序を使用しており、予期せぬ問題を引き起こす可能性があります。
+self_check.database_fix_mysql=MySQL/MariaDBユーザーの方は、"gitea doctor convert" コマンドを使用することで、照合順序の問題を修正できます。 また、"ALTER ... COLLATE ..." のSQLを手で実行しても修正することができます。
+self_check.database_fix_mssql=MSSQLユーザーの方は、問題を修正するには今のところ "ALTER ... COLLATE ..." のSQLを手で実行するしかありません。
+
[action]
create_repo=がリポジトリ %s を作成しました
rename_repo=がリポジトリ名を %[1]s
から %[3]s へ変更しました
@@ -3270,9 +3342,9 @@ raw_seconds=秒
raw_minutes=分
[dropzone]
-default_message=ここにファイルをドロップまたはクリックしてアップロードします。
+default_message=ファイルをここにドロップ、またはここをクリックしてアップロード
invalid_input_type=この種類のファイルはアップロードできません。
-file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) が最大サイズ ({{maxFilesize}} MB) を超えています。
+file_too_big=アップロードされたファイルのサイズ ({{filesize}} MB) は、最大サイズ ({{maxFilesize}} MB) を超えています。
remove_file=ファイル削除
[notification]
@@ -3297,7 +3369,7 @@ error.no_committer_account=コミッターのメールアドレスに対応す
error.no_gpg_keys_found=この署名に対応する既知のキーがデータベースに存在しません
error.not_signed_commit=署名されたコミットではありません
error.failed_retrieval_gpg_keys=コミッターのアカウントに登録されたキーを取得できませんでした
-error.probable_bad_signature=警告! このIDの鍵はデータベースに登録されていますが、その鍵でコミットの検証が通りません! これは疑わしいコミットです。
+error.probable_bad_signature=警告! このIDに該当する鍵がデータベースにありますが、コミットの検証が通りません! これは疑わしいコミットです。
error.probable_bad_default_signature=警告! これはデフォルト鍵のIDですが、デフォルト鍵ではコミットの検証が通りません! これは疑わしいコミットです。
[units]
@@ -3310,7 +3382,7 @@ title=パッケージ
desc=リポジトリ パッケージを管理します。
empty=パッケージはまだありません。
empty.documentation=パッケージレジストリの詳細については、 ドキュメント を参照してください。
-empty.repo=パッケージはアップロードしたけども、ここに表示されない? パッケージ設定を開いて、パッケージをこのリポジトリにリンクしてください。
+empty.repo=パッケージはアップロード済みで、ここに表示されていないですか? パッケージ設定を開いて、パッケージをこのリポジトリにリンクしてください。
registry.documentation=%sレジストリの詳細については、 ドキュメント を参照してください。
filter.type=タイプ
filter.type.all=すべて
@@ -3400,6 +3472,9 @@ rpm.registry=このレジストリをコマンドラインからセットアッ
rpm.distros.redhat=RedHat系ディストリビューションの場合
rpm.distros.suse=SUSE系ディストリビューションの場合
rpm.install=パッケージをインストールするには、次のコマンドを実行します:
+rpm.repository=リポジトリ情報
+rpm.repository.architectures=Architectures
+rpm.repository.multiple_groups=このパッケージは複数のグループで利用可能です。
rubygems.install=gem を使用してパッケージをインストールするには、次のコマンドを実行します:
rubygems.install2=または Gemfile に追加します:
rubygems.dependencies.runtime=実行用依存関係
@@ -3526,14 +3601,15 @@ runs.scheduled=スケジュール済み
runs.pushed_by=pushed by
runs.invalid_workflow_helper=ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s
runs.no_matching_online_runner_helper=ラベルに一致するオンラインのランナーが見つかりません: %s
+runs.no_job_without_needs=ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。
runs.actor=アクター
runs.status=ステータス
runs.actors_no_select=すべてのアクター
runs.status_no_select=すべてのステータス
runs.no_results=一致する結果はありません。
runs.no_workflows=ワークフローはまだありません。
-runs.no_workflows.quick_start=Gitea Action の始め方がわからない? クイックスタートガイドをご覧ください。
-runs.no_workflows.documentation=Gitea Action の詳細については、ドキュメントを参照してください。
+runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? ではクイックスタートガイドをご覧ください。
+runs.no_workflows.documentation=Gitea Actions の詳細については、ドキュメントを参照してください。
runs.no_runs=ワークフローはまだ実行されていません。
runs.empty_commit_message=(空のコミットメッセージ)
@@ -3552,7 +3628,7 @@ variables.none=変数はまだありません。
variables.deletion=変数を削除
variables.deletion.description=変数の削除は恒久的で元に戻すことはできません。 続行しますか?
variables.description=変数は特定のActionsに渡されます。 それ以外で読み出されることはありません。
-variables.id_not_exist=idが%dの変数は存在しません。
+variables.id_not_exist=IDが%dの変数は存在しません。
variables.edit=変数の編集
variables.deletion.failed=変数を削除できませんでした。
variables.deletion.success=変数を削除しました。
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index 1c79ee6bc7..3e9679575c 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -84,6 +84,12 @@ concept_user_organization=조직
name=이름
+filter.is_template=템플릿
+filter.private=비공개
+
+
+[search]
+
[aria]
[heatmap]
@@ -199,7 +205,6 @@ collaborative_repos=협업 저장소
my_orgs=내 조직
my_mirrors=내 미러 저장소들
view_home=%s 보기
-search_repos=저장소 찾기..
show_private=비공개
@@ -210,12 +215,7 @@ issues.in_your_repos=당신의 저장소에
repos=저장소
users=유저
organizations=조직
-search=검색
code=코드
-repo_no_results=일치하는 레포지토리가 없습니다.
-user_no_results=일치하는 사용자가 없습니다.
-org_no_results=일치하는 조직이 없습니다.
-code_no_results=검색어와 일치하는 소스코드가 없습니다.
[auth]
create_new_account=계정 등록
@@ -226,7 +226,6 @@ disable_register_mail=계정 등록을 위한 이메일 검증이 비활성화
forgot_password_title=비밀번호 찾기
forgot_password=비밀번호를 잊으셨나요?
sign_up_now=계정이 필요하신가요? 지금 가입하세요.
-confirmation_mail_sent_prompt=새로운 확인 메일이 %s로 전송되었습니다. 받은 편지함으로 도착한 메일을 %s 안에 확인해서 등록 절차를 완료하십시오.
must_change_password=비밀번호를 변경하세요.
allow_password_change=사용자에게 비밀번호 변경을 요청 (권장됨)
reset_password_mail_sent_prompt=확인 메일이 %s로 전송되었습니다. 받은 편지함으로 도착한 메일을 %s 안에 확인해서 비밀번호 찾기 절차를 완료하십시오.
@@ -349,6 +348,7 @@ auth_failed=인증 실패: %v
target_branch_not_exist=대상 브랜치가 존재하지 않습니다.
+
[user]
change_avatar=아바타 변경
repositories=저장소
@@ -362,6 +362,7 @@ unfollow=추적해제
user_bio=소개
+
[settings]
profile=프로필
account=계정
@@ -660,8 +661,6 @@ editor.add_subdir=경로 추가...
commits.desc=소스 코드 변경 내역 탐색
commits.commits=커밋
-commits.search=커밋 찾기...
-commits.find=검색
commits.search_all=모든 브랜치
commits.author=작성자
commits.message=메시지
@@ -843,7 +842,6 @@ pulls.compare_changes=새 풀 리퀘스트
pulls.compare_base=병합하기
pulls.compare_compare=다음으로부터 풀
pulls.filter_branch=Filter Branch
-pulls.no_results=결과 없음
pulls.create=풀 리퀘스트 생성
pulls.title_desc="%[2]s
에서 %[3]s
로 %[1]d commits 를 머지하려 합니다"
pulls.merged_title_desc=%[2]s
에서 %[3]s
로 %[1]d commits 를 머지했습니다 %[4]s
@@ -949,10 +947,7 @@ activity.title.releases_n=%d 개의 릴리즈
activity.title.releases_published_by=%s 가 %s 에 의하여 배포되었습니다.
activity.published_release_label=배포됨
-search=검색
-search.search_repo=저장소 검색
-search.results="%s 에서 \"%s\" 에 대한 검색 결과"
-search.code_no_results=검색어와 일치하는 소스코드가 없습니다.
+contributors.contribution_type.commits=커밋
settings=설정
settings.desc=설정은 저장소 설정을 관리할 수 있습니다.
@@ -1013,7 +1008,6 @@ settings.add_collaborator=새 공동작업자 추가
settings.add_collaborator_success=공동작업자가 추가 되었습니다.
settings.delete_collaborator=제거
settings.collaborator_deletion=공동작업자 삭제
-settings.search_user_placeholder=사용자 검색...
settings.teams=팀
settings.add_webhook=Webhook 추가
settings.webhook_deletion=Webhook 삭제
@@ -1087,8 +1081,6 @@ settings.branch_protection='%s' 브랜치 보호
settings.protect_this_branch=브랜치 보호 활성화
settings.protect_disable_push=푸시 끄기
settings.protect_enable_push=푸시 켜기
-settings.protect_whitelist_search_users=사용자 찾기...
-settings.protect_whitelist_search_teams=팀 찾기...
settings.protect_merge_whitelist_committers=머지 화이트리스트 활성화
settings.protect_required_approvals=필요한 승인:
settings.protect_approvals_whitelist_users=화이트리스트된 리뷰어:
@@ -1161,6 +1153,8 @@ topic.count_prompt=25개 이상의 토픽을 선택하실 수 없습니다.
+[graphs]
+
[org]
org_name_holder=조직 이름
org_full_name_holder=조직 전체 이름
@@ -1221,7 +1215,6 @@ teams.add_team_member=팀 구성원 추가
teams.delete_team_title=팀 삭제
teams.delete_team_success=팀이 삭제되었습니다.
teams.repositories=팀 저장소
-teams.search_repo_placeholder=저장소 찾기...
teams.add_duplicate_users=사용자가 이미 팀 멤버입니다.
teams.members.none=이 팀에 멤버가 없습니다.
@@ -1232,6 +1225,8 @@ organizations=조직
repositories=저장소
authentication=인증 소스
config=설정
+config_summary=요약
+config_settings=설정
notices=시스템 공지
monitor=모니터링
first_page=처음
@@ -1318,9 +1313,6 @@ repos.repo_manage_panel=저장소 관리
repos.owner=소유자
repos.name=이름
repos.private=비공개
-repos.watches=지켜보기
-repos.stars=별
-repos.forks=포크
repos.issues=이슈
repos.size=크기
@@ -1521,6 +1513,7 @@ notices.desc=설명
notices.op=일.
notices.delete_success=시스템 알림이 삭제되었습니다.
+
[action]
create_repo=저장소를 만들었습니다. %s
rename_repo=%[1]s에서
에서 %[3]s으로 저장소 이름을 바꾸었습니다.
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index e275b02ba0..9a15090012 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -17,10 +17,11 @@ template=Sagatave
language=Valoda
notifications=Paziņojumi
active_stopwatch=Aktīvā laika uzskaite
+tracked_time_summary=Izsekojamā laika apkopojums, kas ir balstīts uz pieteikumu saraksta atlasi
create_new=Izveidot…
user_profile_and_more=Profils un iestatījumi…
signed_in_as=Pieteicies kā
-enable_javascript=Šai lapas darbībai ir nepieciešams JavaScript.
+enable_javascript=Šai tīmekļvietnei ir nepieciešams JavaScript.
toc=Satura rādītājs
licenses=Licences
return_to_gitea=Atgriezties Gitea
@@ -40,12 +41,12 @@ webauthn_sign_in=Nospiediet pogu uz drošības atslēgas. Ja tai nav pogas, izņ
webauthn_press_button=Nospiediet drošības atslēgas pogu…
webauthn_use_twofa=Izmantot divfaktoru kodu no tālruņa
webauthn_error=Nevar nolasīt drošības atslēgu.
-webauthn_unsupported_browser=Jūsu pārlūkprogramma neatbalsta WebAuthn standartu.
+webauthn_unsupported_browser=Jūsu pārlūks neatbalsta WebAuthn standartu.
webauthn_error_unknown=Notikusi nezināma kļūda. Atkārtojiet darbību vēlreiz.
-webauthn_error_insecure=WebAuthn atbalsta tikai drošus savienojumus ar serveri
-webauthn_error_unable_to_process=Serveris nevar apstrādāt Jūsu pieprasījumu.
+webauthn_error_insecure=`WebAuthn atbalsta tikai drošus savienojumus. Pārbaudīšanai ar HTTP var izmantot izcelsmi "localhost" vai "127.0.0.1"`
+webauthn_error_unable_to_process=Serveris nevarēja apstrādāt pieprasījumu.
webauthn_error_duplicated=Drošības atslēga nav atļauta šim pieprasījumam. Pārliecinieties, ka šī atslēga jau nav reģistrēta.
-webauthn_error_empty=Norādiet atslēgas nosaukumu.
+webauthn_error_empty=Jānorāda šīs atslēgas nosaukums.
webauthn_error_timeout=Iestājusies noildze, mēģinot, nolasīt atslēgu. Pārlādējiet lapu un mēģiniet vēlreiz.
webauthn_reload=Pārlādēt
@@ -60,11 +61,11 @@ new_org=Jauna organizācija
new_project=Jauns projekts
new_project_column=Jauna kolonna
manage_org=Pārvaldīt organizācijas
-admin_panel=Lapas administrēšana
+admin_panel=Vietnes administrēšana
account_settings=Konta iestatījumi
settings=Iestatījumi
your_profile=Profils
-your_starred=Atzīmēts ar zvaigznīti
+your_starred=Pievienots izlasē
your_settings=Iestatījumi
all=Visi
@@ -90,9 +91,11 @@ remove=Noņemt
remove_all=Noņemt visus
remove_label_str=`Noņemt ierakstu "%s"`
edit=Labot
+view=Skatīt
enabled=Iespējots
disabled=Atspējots
+locked=Slēgts
copy=Kopēt
copy_url=Kopēt saiti
@@ -109,6 +112,7 @@ loading=Notiek ielāde…
error=Kļūda
error404=Lapa, ko vēlaties atvērt, neeksistē vai arī Jums nav tiesības to aplūkot.
+go_back=Atgriezties
never=Nekad
unknown=Nezināms
@@ -130,12 +134,22 @@ concept_user_organization=Organizācija
show_timestamps=Rādīt laika zīmogus
show_log_seconds=Rādīt sekundes
show_full_screen=Atvērt pilnā logā
+download_logs=Lejupielādēt žurnālus
confirm_delete_selected=Apstiprināt, lai izdzēstu visus atlasītos vienumus?
name=Nosaukums
value=Vērtība
+filter=Filtrs
+filter.is_archived=Arhivētie
+filter.is_template=Sagatave
+filter.public=Publiska
+filter.private=Privāts
+
+
+[search]
+
[aria]
navbar=Navigācijas josla
footer=Kājene
@@ -170,6 +184,7 @@ string.desc=Z - A
[error]
occurred=Radusies kļūda
+report_message=Ja ir pārliecība, ka šī ir Gitea nepilnība, lūgums pārbaudīt GitHub, vai tā jau nav zināma, vai izveidot jaunu pieteikumu, ja nepieciešams.
missing_csrf=Kļūdains pieprasījums: netika iesūtīta drošības pilnvara
invalid_csrf=Kļūdains pieprasījums: iesūtīta kļūdaina drošības pilnvara
not_found=Pieprasītie dati netika atrasti.
@@ -178,6 +193,7 @@ network_error=Tīkla kļūda
[startpage]
app_desc=Viegli uzstādāms Git serviss
install=Vienkārši instalējams
+install_desc=Vienkārši jāpalaiž izpildāmais fails vajadzīgajai platformai, jāizmanto Docker, vai jāiegūst pakotne.
platform=Pieejama dažādām platformām
platform_desc=Gitea iespējams uzstādīt jebkur, kam Go var nokompilēt: Windows, macOS, Linux, ARM utt. Izvēlies to, kas tev patīk!
lightweight=Viegla
@@ -222,6 +238,7 @@ repo_path_helper=Git repozitoriji tiks glabāti šajā direktorijā.
lfs_path=Git LFS glabāšanas vieta
lfs_path_helper=Faili, kas pievienoti Git LFS, tiks glabāti šajā direktorijā. Atstājiet tukšu, lai atspējotu.
run_user=Izpildes lietotājs
+run_user_helper=Operētājsistēms lietotājs, ar kuru tiks palaists Gitea. Jāņem vērā, ka šim lietotājam ir jābūt piekļuvei repozitorija atrašanās vietai.
domain=Servera domēns
domain_helper=Domēns vai servera adrese.
ssh_port=SSH servera ports
@@ -293,6 +310,8 @@ invalid_password_algorithm=Kļūdaina paroles jaucējfunkcija
password_algorithm_helper=Norādiet paroles jaucējalgoritmu. Algoritmi atšķirās pēc prasībām pret resursiem un stipruma. Argon2 algoritms ir drošs, bet tam nepieciešams daudz operatīvās atmiņas, līdz ar ko tas var nebūt piemērots sistēmām ar maz pieejamajiem resursiem.
enable_update_checker=Iespējot jaunu versiju paziņojumus
enable_update_checker_helper=Periodiski pārbaudīt jaunu version pieejamību, izgūstot datus no gitea.io.
+env_config_keys=Vides konfigurācija
+env_config_keys_prompt=Šie vides mainīgie tiks pielietoti arī konfigurācijas failā:
[home]
uname_holder=Lietotājvārds vai e-pasts
@@ -304,7 +323,6 @@ collaborative_repos=Sadarbības repozitoriji
my_orgs=Manas organizācijas
my_mirrors=Mani spoguļi
view_home=Skatīties %s
-search_repos=Meklēt repozitoriju…
filter=Citi filtri
filter_by_team_repositories=Filtrēt pēc komandas repozitorijiem
feed_of=`"%s" plūsma`
@@ -325,20 +343,8 @@ issues.in_your_repos=Jūsu repozitorijos
repos=Repozitoriji
users=Lietotāji
organizations=Organizācijas
-search=Meklēt
go_to=Iet uz
code=Kods
-search.type.tooltip=Meklēšanas veids
-search.fuzzy=Aptuveni
-search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības
-search.match=Precīzi
-search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības
-code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru.
-repo_no_results=Netika atrasts neviens repozitorijs, kas atbilstu kritērijiem.
-user_no_results=Netika atrasts neviens lietotājs, kas atbilstu kritērijiem.
-org_no_results=Netika atrasta neviena organizācija, kas atbilstu kritērijiem.
-code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem.
-code_search_results=`Meklēšanas rezultāti "%s"`
code_last_indexed_at=Pēdējo reizi indeksēts %s
relevant_repositories_tooltip=Repozitoriju, kas ir atdalīti vai kuriem nav tēmas, ikonas un apraksta ir paslēpti.
relevant_repositories=Tikai būtiskie repozitoriji tiek rādīti, pārādīt nefiltrētus rezultātus.
@@ -351,10 +357,11 @@ disable_register_prompt=Reģistrācija ir atspējota. Lūdzu, sazinieties ar vie
disable_register_mail=Reģistrācijas e-pasta apstiprināšana ir atspējota.
manual_activation_only=Sazinieties ar lapas administratoru, lai pabeigtu konta aktivizāciju.
remember_me=Atcerēties šo ierīci
+remember_me.compromised=Pieteikšanās pilnvara vairs nav derīga, kas var norādīt uz ļaunprātīgām darbībām kontā. Lūgums pārbaudīt, vai kontā nav neparastu darbību.
forgot_password_title=Aizmirsu paroli
forgot_password=Aizmirsi paroli?
sign_up_now=Nepieciešams konts? Reģistrējies tagad.
-confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz %s, pārbaudies savu e-pasta kontu tuvāko %s laikā, lai pabeigtu reģistrācijas procesu.
+sign_up_successful=Konts tika veiksmīgi izveidots. Laipni lūdzam!
must_change_password=Mainīt paroli
allow_password_change=Pieprasīt lietotājam mainīt paroli (ieteicams)
reset_password_mail_sent_prompt=Apstiprināšanas e-pasts tika nosūtīts uz %s. Pārbaudiet savu e-pasta kontu tuvāko %s laikā, lai pabeigtu paroles atjaunošanas procesu.
@@ -369,6 +376,7 @@ email_not_associate=Šī e-pasta adrese nav saistīta ar nevienu kontu.
send_reset_mail=Nosūtīt paroles atjaunošanas e-pastu
reset_password=Paroles atjaunošana
invalid_code=Jūsu apstiprināšanas kodam ir beidzies derīguma termiņš vai arī tas ir nepareizs.
+invalid_code_forgot_password=Apliecinājuma kods ir nederīgs vai tā derīgums ir beidzies. Nospiediet šeit, lai uzsāktu jaunu sesiju.
invalid_password=Jūsu parole neatbilst parolei, kas tika ievadīta veidojot so kontu.
reset_password_helper=Atjaunot paroli
reset_password_wrong_user=Jūs esat pieteicies kā %s, bet konta atkopšanas saite ir paredzēta lietotājam %s
@@ -396,6 +404,7 @@ openid_connect_title=Pievienoties jau esošam kontam
openid_connect_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam.
openid_register_title=Izveidot jaunu kontu
openid_register_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam.
+openid_signin_desc=Jāievada OpenID URI. Piemēram, anna.openid.example.org vai https://openid.example.org/anna.
disable_forgot_password_mail=Konta atjaunošana ir atspējota, jo nav uzstādīti e-pasta servera iestatījumi. Sazinieties ar lapas administratoru.
disable_forgot_password_mail_admin=Kontu atjaunošana ir pieejama tikai, ja ir veikta e-pasta servera iestatījumu konfigurēšana. Norādiet e-pasta servera iestatījumus, lai iespējotu kontu atjaunošanu.
email_domain_blacklisted=Nav atļauts reģistrēties ar šādu e-pasta adresi.
@@ -405,7 +414,9 @@ authorize_application_created_by=Šo lietotni izveidoja %s.
authorize_application_description=Ja piešķirsiet tiesības, tā varēs piekļūt un mainīt Jūsu konta informāciju, ieskaitot privātos repozitorijus un organizācijas.
authorize_title=Autorizēt "%s" piekļuvi jūsu kontam?
authorization_failed=Autorizācija neizdevās
+authorization_failed_desc=Autentifikācija neizdevās, jo tika veikts kļūdains pieprasījums. Sazinieties ar lietojumprogrammas, ar kuru mēģinājāt autentificēties, uzturētāju.
sspi_auth_failed=SSPI autentifikācija neizdevās
+password_pwned=Izvēlētā parole ir nozagto paroļu sarakstā, kas iepriekš ir atklāts publiskās datu noplūdēs. Lūgums mēģināt vēlreiz ar citu paroli un apsvērt to nomainīt arī citur.
password_pwned_err=Neizdevās pabeigt pieprasījumu uz HaveIBeenPwned
[mail]
@@ -420,6 +431,7 @@ activate_account.text_1=Sveiki %[1]s, esat reģistrējies %[2]s!
activate_account.text_2=Nospiediet uz saites, lai aktivizētu savu kontu lapā %s:
activate_email=Apstipriniet savu e-pasta adresi
+activate_email.title=%s, apstipriniet savu e-pasta adresi
activate_email.text=Nospiediet uz saites, lai apstiprinātu savu e-pasta adresi lapā %s:
register_notify=Laipni lūdzam Gitea
@@ -571,6 +583,7 @@ org_still_own_packages=Šai organizācijai pieder viena vai vārākas pakotnes,
target_branch_not_exist=Mērķa atzars neeksistē
+
[user]
change_avatar=Mainīt profila attēlu…
joined_on=Pievienojās %s
@@ -589,11 +602,14 @@ user_bio=Biogrāfija
disabled_public_activity=Šis lietotājs ir atslēdzies iespēju aplūkot tā aktivitāti.
email_visibility.limited=E-pasta adrese ir redzama visiem autentificētajiem lietotājiem
email_visibility.private=E-pasta adrese ir redzama tikai administratoriem
+show_on_map=Rādīt šo vietu kartē
+settings=Lietotāja iestatījumi
form.name_reserved=Lietotājvārdu "%s" nedrīkst izmantot.
form.name_pattern_not_allowed=Lietotājvārds "%s" nav atļauts.
form.name_chars_not_allowed=Lietotāja vārds "%s" satur neatļautus simbolus.
+
[settings]
profile=Profils
account=Konts
@@ -610,9 +626,13 @@ delete=Dzēst kontu
twofa=Divfaktoru autentifikācija
account_link=Saistītie konti
organization=Organizācijas
+uid=UID
webauthn=Drošības atslēgas
public_profile=Publiskais profils
+biography_placeholder=Pastāsti mums mazliet par sevi! (Var izmantot Markdown)
+location_placeholder=Kopīgot savu aptuveno atrašanās vietu ar citiem
+profile_desc=Norādīt, kā profils tiek attēlots citiem lietotājiem. Primārā e-pasta adrese tiks izmantota paziņojumiem, paroles atjaunošanai un Git tīmekļa darbībām.
password_username_disabled=Ne-lokāliem lietotājiem nav atļauts mainīt savu lietotāja vārdu. Sazinieties ar sistēmas administratoru, lai uzzinātu sīkāk.
full_name=Pilns vārds
website=Mājas lapa
@@ -624,6 +644,8 @@ update_language_not_found=Valoda "%s" nav pieejama.
update_language_success=Valoda tika nomainīta.
update_profile_success=Jūsu profila informācija tika saglabāta.
change_username=Lietotājvārds mainīts.
+change_username_prompt=Piezīme: lietotājvārda mainīšana maina arī konta URL.
+change_username_redirect_prompt=Iepriekšējais lietotājvārds tiks pārvirzīts, kamēr neviens cits to neizmanto.
continue=Turpināt
cancel=Atcelt
language=Valoda
@@ -648,6 +670,7 @@ comment_type_group_project=Projektus
comment_type_group_issue_ref=Problēmu atsauces
saved_successfully=Iestatījumi tika veiksmīgi saglabati.
privacy=Privātums
+keep_activity_private=Profila lapā paslēpt notikumus
keep_activity_private_popup=Savu aktivitāti redzēsiet tikai Jūs un administratori
lookup_avatar_by_mail=Meklēt profila bildes pēc e-pasta
@@ -657,12 +680,14 @@ choose_new_avatar=Izvēlēties jaunu profila attēlu
update_avatar=Saglabāt profila bildi
delete_current_avatar=Dzēst pašreizējo profila bildi
uploaded_avatar_not_a_image=Augšupielādētais fails nav attēls.
+uploaded_avatar_is_too_big=Augšupielādētā faila izmērs (%d KiB) pārsniedz pieļaujamo izmēru (%d KiB).
update_avatar_success=Profila attēls tika saglabāts.
update_user_avatar_success=Lietotāja profila attēls tika atjaunots.
change_password=Mainīt paroli
old_password=Pašreizējā parole
new_password=Jauna parole
+retype_new_password=Apstiprināt jauno paroli
password_incorrect=Ievadīta nepareiza pašreizējā parole.
change_password_success=Parole tika veiksmīgi nomainīta. Tagad varat pieteikties ar jauno paroli.
password_change_disabled=Ārējie konti nevar mainīt paroli, izmantojot, Gitea saskarni.
@@ -671,6 +696,7 @@ emails=E-pasta adreses
manage_emails=Pārvaldīt e-pasta adreses
manage_themes=Izvēlieties noklusējuma motīvu
manage_openid=Pārvaldīt OpenID adreses
+email_desc=Primārā e-pasta adrese tiks izmantota paziņojumiem, paroļu atjaunošanai un, ja tā nav paslēpta, Git tīmekļa darbībām.
theme_desc=Šis būs noklusējuma motīvs visiem lietotājiem.
primary=Primārā
activated=Aktivizēts
@@ -678,6 +704,7 @@ requires_activation=Nepieciešams aktivizēt
primary_email=Uzstādīt kā primāro
activate_email=Nosūtīt aktivizācijas e-pastu
activations_pending=Gaida aktivizāciju
+can_not_add_email_activations_pending=Ir nepabeigta aktivizācija. Pēc dažām minūtēm mēģiniet vēlreiz, ja ir vēlme pievienot jaunu e-pasta adresi.
delete_email=Noņemt
email_deletion=Dzēst e-pasta adresi
email_deletion_desc=E-pasta adrese un ar to saistītā informācija tiks dzēsta no šī konta. Git revīzijas ar šo e-pasta adresi netiks mainītas. Vai turpināt?
@@ -696,6 +723,7 @@ add_email_success=Jūsu jaunā e-pasta adrese tika veiksmīgi pievienota.
email_preference_set_success=E-pasta izvēle tika veiksmīgi saglabāta.
add_openid_success=Jūsu jaunā OpenID adrese tika veiksmīgi pievienota.
keep_email_private=Paslēpt e-pasta adresi
+keep_email_private_popup=Šis profilā paslēps e-pasta adresi, kā arī tad, kad tiks veikts izmaiņu pieprasījums vai tīmekļa saskarnē labota datne. Aizgādātie iesūtījumi netiks pārveidoti. Revīzijās jāizmanto %s, lai sasaistītu tos ar kontu.
openid_desc=Jūsu OpenID adreses ļauj autorizēties, izmantojot, Jūsu izvēlēto pakalpojumu sniedzēju.
manage_ssh_keys=Pārvaldīt SSH atslēgas
@@ -726,7 +754,6 @@ gpg_invalid_token_signature=Norādītā GPG atslēga, paraksts un pilnvara neatb
gpg_token_required=Jānorāda paraksts zemāk esošajai pilnvarai
gpg_token=Pilnvara
gpg_token_help=Parakstu ir iespējams uzģenerēt izmantojot komandu:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Tekstuāls GPG paraksts
key_signature_gpg_placeholder=Sākas ar '-----BEGIN PGP SIGNATURE-----'
verify_gpg_key_success=GPG atslēga "%s" veiksmīgi pārbaudīta.
@@ -776,6 +803,7 @@ ssh_externally_managed=Šim lietotājam SSH atslēga tiek pāvaldīta attālinā
manage_social=Pārvaldīt piesaistītos sociālos kontus
social_desc=Šie sociālo tīklu konti var tikt izmantoti, lai pieteiktos. Pārliecinieties, ka visi ir atpazīstami.
unbind=Atsaistīt
+unbind_success=Sociālā tīkla konts tika veiksmīgi noņemts.
manage_access_token=Pārvaldīt piekļuves pilnvaras
generate_new_token=Izveidot jaunu pilnvaru
@@ -795,7 +823,9 @@ permissions_public_only=Tikai publiskie
permissions_access_all=Visi (publiskie, privātie un ierobežotie)
select_permissions=Norādiet tiesības
permission_no_access=Nav piekļuves
-permission_read=Izlasītie
+permission_read=Skatīšanās
+permission_write=Skatīšanās un raksīšanas
+access_token_desc=Atzīmētie pilnvaras apgabali ierobežo autentifikāciju tikai atbilstošiem API izsaukumiem. Sīkāka informācija pieejama dokumentācijā.
at_least_one_permission=Nepieciešams norādīt vismaz vienu tiesību, lai izveidotu pilnvaru
permissions_list=Tiesības:
@@ -807,6 +837,8 @@ remove_oauth2_application_desc=Noņemot OAuth2 lietotni, tiks noņemta piekļuve
remove_oauth2_application_success=Lietotne tika dzēsta.
create_oauth2_application=Izveidot jaunu OAuth2 lietotni
create_oauth2_application_button=Izveidot lietotni
+create_oauth2_application_success=Ir veiksmīgi izveidota jauna OAuth2 lietotne.
+update_oauth2_application_success=Ir veiksmīgi atjaunota OAuth2 lietotne.
oauth2_application_name=Lietotnes nosaukums
oauth2_confidential_client=Konfidenciāls klients. Norādiet lietotēm, kas glabā noslēpumu slepenībā, piemēram, tīmekļa lietotnēm. Nenorādiet instalējamām lietotnēm, tai skaitā darbavirsmas vai mobilajām lietotnēm.
oauth2_redirect_uris=Pārsūtīšanas URI. Norādiet katru URI savā rindā.
@@ -815,19 +847,26 @@ oauth2_client_id=Klienta ID
oauth2_client_secret=Klienta noslēpums
oauth2_regenerate_secret=Pārģenerēt noslēpumus
oauth2_regenerate_secret_hint=Pazaudēts noslēpums?
+oauth2_client_secret_hint=Pēc šīs lapas pamešanas vai atsvaidzināšanas noslēpums vairs netiks parādīts. Lūgums pārliecināties, ka tas ir saglabāts.
oauth2_application_edit=Labot
oauth2_application_create_description=OAuth2 lietotnes ļauj trešas puses lietotnēm piekļūt lietotāja kontiem šajā instancē.
+oauth2_application_remove_description=OAuth2 lietotnes noņemšana liegs tai piekļūt pilnvarotiem lietotāju kontiem šajā instancē. Vai turpināt?
+oauth2_application_locked=Gitea sāknēšanas brīdī reģistrē dažas OAuth2 lietotnes, ja tas ir iespējots konfigurācijā. Lai novērstu negaidītu uzvedību, tās nevar ne labot, ne noņemt. Lūgums vērsties OAuth2 dokumentācijā pēc vairāk informācijas.
authorized_oauth2_applications=Autorizētās OAuth2 lietotnes
+authorized_oauth2_applications_description=Ir ļauta piekļuve savam Gitea kontam šīm trešo pušu lietotnēm. Lūgums atsaukt piekļuvi lietotnēm, kas vairs nav nepieciešamas.
revoke_key=Atsaukt
revoke_oauth2_grant=Atsaukt piekļuvi
revoke_oauth2_grant_description=Atsaucot piekļuvi šai trešas puses lietotnei tiks liegta piekļuve Jūsu datiem. Vai turpināt?
+revoke_oauth2_grant_success=Piekļuve veiksmīgi atsaukta.
twofa_desc=Divfaktoru autentifikācija uzlabo konta drošību.
+twofa_recovery_tip=Ja ierīce tiek pazaudēta, iespējams izmantot vienreiz izmantojamo atkopšanas atslēgu, lai atgūtu piekļuvi savam kontam.
twofa_is_enrolled=Kontam ir ieslēgta divfaktoru autentifikācija.
twofa_not_enrolled=Kontam šobrīd nav ieslēgta divfaktoru autentifikācija.
twofa_disable=Atslēgt divfaktoru autentifikāciju
twofa_scratch_token_regenerate=Ģenerēt jaunu vienreizējo kodu
+twofa_scratch_token_regenerated=Vienreizējā pilnvara tagad ir %s. Tā ir jāglabā drošā vietā, tā vairs nekad netiks rādīta.
twofa_enroll=Ieslēgt divfaktoru autentifikāciju
twofa_disable_note=Nepieciešamības gadījumā divfaktoru autentifikāciju ir iespējams atslēgt.
twofa_disable_desc=Atslēdzot divfaktoru autentifikāciju, konts vairs nebūs tik drošs. Vai turpināt?
@@ -845,6 +884,8 @@ webauthn_register_key=Pievienot drošības atslēgu
webauthn_nickname=Segvārds
webauthn_delete_key=Noņemt drošības atslēgu
webauthn_delete_key_desc=Noņemot drošības atslēgu ar to vairs nebūs iespējams pieteikties. Vai turpināt?
+webauthn_key_loss_warning=Ja tiek pazaudētas drošības atslēgas, tiks zaudēta piekļuve kontam.
+webauthn_alternative_tip=Ir vēlams uzstādīt papildu autentifikācijas veidu.
manage_account_links=Pārvaldīt saistītos kontus
manage_account_links_desc=Šādi ārējie konti ir piesaistīti Jūsu Gitea kontam.
@@ -854,8 +895,10 @@ remove_account_link=Noņemt saistīto kontu
remove_account_link_desc=Noņemot saistīto kontu, tam tiks liegta piekļuve Jūsu Gitea kontam. Vai turpināt?
remove_account_link_success=Saistītais konts tika noņemts.
+hooks.desc=Pievienot tīmekļa āķus, kas izpildīsies visos repozitorijos, kas jums pieder.
orgs_none=Jūs neesat nevienas organizācijas biedrs.
+repos_none=Jums nepieder neviens repozitorijs.
delete_account=Dzēst savu kontu
delete_prompt=Šī darbība pilnībā izdzēsīs Jūsu kontu, kā arī tā ir NEATGRIEZENISKA.
@@ -874,9 +917,12 @@ visibility=Lietotāja redzamība
visibility.public=Publisks
visibility.public_tooltip=Redzams ikvienam
visibility.limited=Ierobežota
+visibility.limited_tooltip=Redzams tikai autentificētiem lietotājiem
visibility.private=Privāts
+visibility.private_tooltip=Redzams tikai organizāciju, kurām esi pievienojies, dalībniekiem
[repo]
+new_repo_helper=Repozitorijs satur visus projekta failus, tajā skaitā izmaiņu vēsturi. Jau tiek glabāts kaut kur citur? Pārnest repozitoriju.
owner=Īpašnieks
owner_helper=Ņemot vērā maksimālā repozitoriju skaita ierobežojumu, ne visas organizācijas var tikt parādītas sarakstā.
repo_name=Repozitorija nosaukums
@@ -888,6 +934,7 @@ template_helper=Padarīt repozitoriju par sagatavi
template_description=Sagatavju repozitoriji tiek izmantoti, lai balstoties uz tiem veidotu jaunus repozitorijus saglabājot direktoriju un failu struktūru.
visibility=Redzamība
visibility_description=Tikai organizācijas īpašnieks vai tās biedri, kam ir tiesības, varēs piekļūt šim repozitorijam.
+visibility_helper=Padarīt repozitoriju privātu
visibility_helper_forced=Jūsu sistēmas administrators ir noteicis, ka visiem no jauna izveidotajiem repozitorijiem ir jābūt privātiem.
visibility_fork_helper=(Šīs vērtības maiņa ietekmēs arī visus atdalītos repozitorijus.)
clone_helper=Nepieciešama palīdzība klonēšanā? Apmeklē palīdzības sadaļu.
@@ -896,8 +943,10 @@ fork_from=Atdalīt no
already_forked=Repozitorijs %s jau ir atdalīts
fork_to_different_account=Atdalīt uz citu kontu
fork_visibility_helper=Atdalītam repozitorijam nav iespējams mainīt tā redzamību.
+fork_branch=Atzars, ko klonēt atdalītajā repozitorijā
+all_branches=Visi atzari
+fork_no_valid_owners=Šim repozitorijam nevar izveidot atdalītu repozitoriju, jo tam nav spēkā esošu īpašnieku.
use_template=Izmantot šo sagatavi
-clone_in_vsc=Atvērt VS Code
download_zip=Lejupielādēt ZIP
download_tar=Lejupielādēt TAR.GZ
download_bundle=Lejupielādēt BUNDLE
@@ -923,7 +972,8 @@ trust_model_helper_committer=Revīzijas iesūtītāja: Uzticēties parakstiem, k
trust_model_helper_collaborator_committer=Līdzstrādnieka un revīzijas iesūtītāja: Uzticēties līdzstrādnieku parakstiem, kas atbilst revīzijas iesūtītājam
trust_model_helper_default=Noklusētais: Izmantojiet šī servera noklusēto uzticamības modeli
create_repo=Izveidot repozitoriju
-default_branch=Noklusējuma atzars
+default_branch=Noklusētais atzars
+default_branch_label=noklusējuma
default_branch_helper=Noklusētais atzars nosaka pamata atzaru uz kuru tiks veidoti izmaiņu pieprasījumi un koda revīziju iesūtīšana.
mirror_prune=Izmest
mirror_prune_desc=Izdzēst visas ārējās atsauces, kas ārējā repozitorijā vairs neeksistē
@@ -932,6 +982,8 @@ mirror_interval_invalid=Nekorekts spoguļošanas intervāls.
mirror_sync_on_commit=Sinhronizēt, kad revīzijas tiek iesūtītas
mirror_address=Spoguļa adrese
mirror_address_desc=Pieslēgšanās rekvizītus norādiet autorizācijas sadaļā.
+mirror_address_url_invalid=Norādītais URL ir nederīgs. Visas URL daļas ir jānorāda pareizi.
+mirror_address_protocol_invalid=Norādītais URL ir nederīgs. Var spoguļot tikai no http(s):// vai git:// adresēm.
mirror_lfs=Lielu failu glabātuve (LFS)
mirror_lfs_desc=Aktivizēt LFS datu spoguļošanu.
mirror_lfs_endpoint=LFS galapunkts
@@ -942,7 +994,7 @@ mirror_password_blank_placeholder=(nav uzstādīts)
mirror_password_help=Nomainiet lietotāju, lai izdzēstu saglabāto paroli.
watchers=Novērotāji
stargazers=Zvaigžņdevēji
-stars_remove_warning=Tiks noņemtas visas atzīmētās zvaigznes šim repozitorijam.
+stars_remove_warning=Šis repozitorijs tiks noņemts no visām izlasēm.
forks=Atdalītie repozitoriji
reactions_more=un vēl %d
unit_disabled=Administrators ir atspējojies šo repozitorija sadaļu.
@@ -957,19 +1009,27 @@ delete_preexisting=Dzēst jau eksistējošos failus
delete_preexisting_content=Dzēst failus direktorijā %s
delete_preexisting_success=Dzēst nepārņemtos failus direktorijā %s
blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas
+blame.ignore_revs=Neņem vērā izmaiņas no .git-blame-ignore-revs. Nospiediet šeit, lai to apietu un redzētu visu izmaiņu skatu.
+blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no .git-blam-ignore-revs.
author_search_tooltip=Tiks attēloti ne vairāk kā 30 lietotāji
+tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s
+tree_path_not_found_branch=Atzarā %[2]s nepastāv ceļš %[1]s
+tree_path_not_found_tag=Tagā %[2]s nepastāv ceļš %[1]s
transfer.accept=Apstiprināt īpašnieka maiņu
transfer.accept_desc=`Mainīt īpašnieku uz "%s"`
transfer.reject=Noraidīt īpašnieka maiņu
transfer.reject_desc=`Atcelt īpašnieka maiņu uz "%s"`
+transfer.no_permission_to_accept=Nav atļaujas pieņemt šo pārsūtīšanu.
+transfer.no_permission_to_reject=Nav atļaujas noraidīt šo pārsūtīšanu.
desc.private=Privāts
desc.public=Publisks
desc.template=Sagatave
desc.internal=Iekšējs
desc.archived=Arhivēts
+desc.sha256=SHA256
template.items=Sagataves ieraksti
template.git_content=Git saturs (noklusētais atzars)
@@ -982,6 +1042,8 @@ template.issue_labels=Problēmu etiķetes
template.one_item=Norādiet vismaz vienu sagataves vienību
template.invalid=Norādiet sagataves repozitoriju
+archive.title=Šis repozitorijs ir arhivēts. Ir iespējams aplūkot tā failus un to konēt, bet nav iespējams iesūtīt izmaiņas, kā arī izveidot jaunas problēmas vai izmaiņu pieprasījumus.
+archive.title_date=Šis repozitorijs tika arhivēts %s. Ir iespējams aplūkot tā failus un to konēt, bet nav iespējams iesūtīt izmaiņas, kā arī izveidot jaunas problēmas vai izmaiņu pieprasījumus.
archive.issue.nocomment=Repozitorijs ir arhivēts. Problēmām nevar pievienot jaunus komentārus.
archive.pull.nocomment=Repozitorijs ir arhivēts. Izmaiņu pieprasījumiem nevar pievienot jaunus komentārus.
@@ -998,6 +1060,7 @@ migrate_options_lfs=Migrēt LFS failus
migrate_options_lfs_endpoint.label=LFS galapunkts
migrate_options_lfs_endpoint.description=Migrācija mēģinās izmantot attālināto URL, lai noteiktu LFS serveri. Var norādīt arī citu galapunktu, ja repozitorija LFS dati ir izvietoti citā vietā.
migrate_options_lfs_endpoint.description.local=Iespējams norādīt arī servera ceļu.
+migrate_options_lfs_endpoint.placeholder=Ja nav norādīts, galamērķis tiks atvasināts no klonēšanas URL
migrate_items=Vienības, ko pārņemt
migrate_items_wiki=Vikivietni
migrate_items_milestones=Atskaites punktus
@@ -1048,11 +1111,11 @@ generated_from=ģenerēts no
fork_from_self=Nav iespējams atdalīt repozitoriju, kuram esat īpašnieks.
fork_guest_user=Piesakieties, lai atdalītu repozitoriju.
watch_guest_user=Piesakieties, lai sekotu šim repozitorijam.
-star_guest_user=Piesakieties, lai atzīmētu šo repozitoriju ar zvaigznīti.
+star_guest_user=Piesakieties, lai pievienotu šo repozitoriju izlasei.
unwatch=Nevērot
watch=Vērot
unstar=Noņemt zvaigznīti
-star=Pievienot zvaigznīti
+star=Pievienot izlasei
fork=Atdalīts
download_archive=Lejupielādēt repozitoriju
more_operations=Vairāk darbību
@@ -1100,6 +1163,10 @@ file_view_rendered=Skatīt rezultātu
file_view_raw=Rādīt neapstrādātu
file_permalink=Patstāvīgā saite
file_too_large=Šis fails ir par lielu, lai to parādītu.
+invisible_runes_header=`Šīs fails satur neredzamus unikoda simbolus`
+invisible_runes_description=`Šis fails satur neredzamus unikoda simbolus, kas ir neatšķirami cilvēkiem, bet dators tās var atstrādāt atšķirīgi. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.`
+ambiguous_runes_header=`Šis fails satur neviennozīmīgus unikoda simbolus`
+ambiguous_runes_description=`Šis fails satur unikoda simbolus, kas var tikt sajauktas ar citām rakstzīmēm. Ja šķiet, ka tas ir ar nolūku, šo brīdinājumu var droši neņemt vērā. Jāizmanto atsoļa taustiņš (Esc), lai atklātu tās.`
invisible_runes_line=`Šī līnija satur neredzamus unikoda simbolus`
ambiguous_runes_line=`Šī līnija satur neviennozīmīgus unikoda simbolus`
ambiguous_character=`%[1]c [U+%04[1]X] var tikt sajaukts ar %[2]c [U+%04[2]X]`
@@ -1112,11 +1179,15 @@ video_not_supported_in_browser=Jūsu pārlūks neatbalsta HTML5 video.
audio_not_supported_in_browser=Jūsu pārlūks neatbalsta HTML5 audio.
stored_lfs=Saglabāts Git LFS
symbolic_link=Simboliska saite
+executable_file=Izpildāmais fails
commit_graph=Revīziju grafs
commit_graph.select=Izvēlieties atzarus
commit_graph.hide_pr_refs=Paslēpt izmaiņu pieprasījumus
commit_graph.monochrome=Melnbalts
commit_graph.color=Krāsa
+commit.contained_in=Šī revīzija ir iekļauta:
+commit.contained_in_default_branch=Šī revīzija ir daļa no noklusētā atzara
+commit.load_referencing_branches_and_tags=Ielādēt atzarus un tagus, kas atsaucas uz šo revīziju
blame=Vainot
download_file=Lejupielādēt failu
normal_view=Parastais skats
@@ -1195,9 +1266,7 @@ commits.desc=Pārlūkot pirmkoda izmaiņu vēsturi.
commits.commits=Revīzijas
commits.no_commits=Nav kopīgu revīziju. Atzariem "%s" un "%s" ir pilnībā atšķirīga izmaiņu vēsture.
commits.nothing_to_compare=Atzari ir vienādi.
-commits.search=Meklēt revīzijas…
commits.search.tooltip=Jūs varat izmantot atslēgas vārdus "author:", "committer:", "after:" vai "before:", piemēram, "revert author:Alice before:2019-01-13".
-commits.find=Meklēt
commits.search_all=Visi atzari
commits.author=Autors
commits.message=Ziņojums
@@ -1209,6 +1278,7 @@ commits.signed_by_untrusted_user=Parakstījis neuzticams lietotājs
commits.signed_by_untrusted_user_unmatched=Parakstījis neuzticams lietotājs, kas neatbilst izmaiņu autoram
commits.gpg_key_id=GPG atslēgas ID
commits.ssh_key_fingerprint=SSH atslēgas identificējošā zīmju virkne
+commits.view_path=Skatīt šajā vēstures punktā
commit.operations=Darbības
commit.revert=Atgriezt
@@ -1219,7 +1289,7 @@ commit.cherry-pick-header=Izlasīt: %s
commit.cherry-pick-content=Norādiet atzaru uz kuru izlasīt:
commitstatus.error=Kļūda
-commitstatus.failure=Neveiksmīgs
+commitstatus.failure=Kļūme
commitstatus.pending=Nav iesūtīts
commitstatus.success=Pabeigts
@@ -1247,7 +1317,6 @@ projects.type.basic_kanban=`Vienkāršots "Kanban"`
projects.type.bug_triage=Kļūdu šķirošana
projects.template.desc=Projekta sagatave
projects.template.desc_helper=Izvēlieties projekta sagatavi, lai sāktu darbu
-projects.type.uncategorized=Bez kategorijas
projects.column.edit=Rediģēt kolonnas
projects.column.edit_title=Nosaukums
projects.column.new_title=Nosaukums
@@ -1255,10 +1324,7 @@ projects.column.new_submit=Izveidot kolonnu
projects.column.new=Jauna kolonna
projects.column.set_default=Izvēlēties kā noklusēto
projects.column.set_default_desc=Izvēlēties šo kolonnu kā noklusēto nekategorizētām problēmām un izmaiņu pieteikumiem
-projects.column.unset_default=Atiestatīt noklusēto
-projects.column.unset_default_desc=Noņemt šo kolonnu kā noklusēto
projects.column.delete=Dzēst kolonnu
-projects.column.deletion_desc=Dzēšot projekta kolonnu visas tam piesaistītās problēmas tiks pārliktas kā nekategorizētas. Vai turpināt?
projects.column.color=Krāsa
projects.open=Aktīvie
projects.close=Pabeigtie
@@ -1336,14 +1402,15 @@ issues.delete_branch_at=`izdzēsa atzaru %s %s`
issues.filter_label=Etiķete
issues.filter_label_exclude=`Izmantojiet alt
+ peles klikšķis vai enter
, lai neiekļautu etiķeti`
issues.filter_label_no_select=Visas etiķetes
+issues.filter_label_select_no_label=Nav etiķetes
issues.filter_milestone=Atskaites punkts
issues.filter_milestone_all=Visi atskaites punkti
issues.filter_milestone_none=Nav atskaites punkta
issues.filter_milestone_open=Atvērtie atskaites punkti
issues.filter_milestone_closed=Aizvērtie atskaites punkti
-issues.filter_project=Projektus
+issues.filter_project=Projekts
issues.filter_project_all=Visi projekti
-issues.filter_project_none=Nav projektu
+issues.filter_project_none=Nav projekta
issues.filter_assignee=Atbildīgais
issues.filter_assginee_no_select=Visi atbildīgie
issues.filter_assginee_no_assignee=Nav atbildīgā
@@ -1389,6 +1456,7 @@ issues.next=Nākamā
issues.open_title=Atvērta
issues.closed_title=Slēgta
issues.draft_title=Melnraksts
+issues.num_comments_1=%d komentārs
issues.num_comments=%d komentāri
issues.commented_at=`komentēja %s`
issues.delete_comment_confirm=Vai patiešām vēlaties dzēst šo komentāru?
@@ -1397,6 +1465,7 @@ issues.context.quote_reply=Atbildēt citējot
issues.context.reference_issue=Atsaukties uz šo jaunā problēmā
issues.context.edit=Labot
issues.context.delete=Dzēst
+issues.no_content=Nav sniegts apraksts.
issues.close=Slēgt problēmu
issues.comment_pull_merged_at=saplidināta revīzija %[1]s atzarā %[2]s %[3]s
issues.comment_manually_pull_merged_at=manuāli saplidināta revīzija %[1]s atzarā %[2]s %[3]s
@@ -1415,8 +1484,17 @@ issues.ref_closed_from=`aizvēra problēmu %[4]s atkārtoti atvēra problēmu %[4]s %[2]s`
issues.ref_from=`no %[1]s`
issues.author=Autors
+issues.author_helper=Šis lietotājs ir autors.
issues.role.owner=Īpašnieks
-issues.role.member=Biedri
+issues.role.owner_helper=Šis lietotājs ir šī repozitorija īpašnieks.
+issues.role.member=Dalībnieks
+issues.role.member_helper=Šis lietotājs ir organizācijas, kurai pieder šis repozitorijs, dalībnieks.
+issues.role.collaborator=Līdzstrādnieks
+issues.role.collaborator_helper=Šis lietotājs ir uzaicināts līdzdarboties repozitorijā.
+issues.role.first_time_contributor=Pirmreizējs līdzradītājs
+issues.role.first_time_contributor_helper=Šis ir pirmais šī lietotāja ieguldījums šājā repozitorijā.
+issues.role.contributor=Līdzradītājs
+issues.role.contributor_helper=Šis lietotājs repozitorijā ir iepriekš veicis labojumus.
issues.re_request_review=Pieprasīt atkārtotu recenziju
issues.is_stale=Šajā izmaiņu pieprasījumā ir notikušas izmaiņās, kopš veicāt tā recenziju
issues.remove_request_review=Noņemt recenzijas pieprasījumu
@@ -1431,6 +1509,9 @@ issues.label_title=Etiķetes nosaukums
issues.label_description=Etiķetes apraksts
issues.label_color=Etiķetes krāsa
issues.label_exclusive=Ekskluzīvs
+issues.label_archive=Arhīvēt etiķeti
+issues.label_archived_filter=Rādīt arhivētās etiķetes
+issues.label_archive_tooltip=Arhivētās etiķetes pēc noklusējuma netiek iekļautas ieteikumos, kad meklē pēc nosaukuma.
issues.label_exclusive_desc=Nosauciet etiķeti grupa/nosaukums
, lai grupētu etiķētes un varētu norādīt tās kā ekskluzīvas ar citām grupa/
etiķetēm.
issues.label_exclusive_warning=Jebkura konfliktējoša ekskluzīvas grupas etiķete tiks noņemta, labojot pieteikumu vai izmaiņu pietikumu etiķetes.
issues.label_count=%d etiķetes
@@ -1485,6 +1566,7 @@ issues.tracking_already_started=`Jau ir uzsākta laika uzskaite par %[2]s#%[3]d`
@@ -1633,6 +1725,12 @@ pulls.is_empty=Mērķa atzars jau satur šī atzara izmaiņas. Šī revīzija b
pulls.required_status_check_failed=Dažas no pārbaudēm nebija veiksmīgas.
pulls.required_status_check_missing=Trūkst dažu obligāto pārbaužu.
pulls.required_status_check_administrator=Kā administrators Jūs varat sapludināt šo izmaiņu pieprasījumu.
+pulls.blocked_by_approvals=Šim izmaiņu pieprasījumam vēl nav pietiekami daudz apstiprinājumu. Nodrošināti %d no %d apstiprinājumiem.
+pulls.blocked_by_rejection=Šim izmaiņu pieprasījumam oficiālais recenzents ir pieprasījis labojumus.
+pulls.blocked_by_official_review_requests=Šim izmaiņu pieprasījumam ir oficiāli recenzijas pieprasījumi.
+pulls.blocked_by_outdated_branch=Šis izmaiņu pieprasījums ir bloķēts, jo tas ir novecojis.
+pulls.blocked_by_changed_protected_files_1=Šis izmaiņu pieprasījums ir bloķēts, jo tas izmaina aizsargāto failu:
+pulls.blocked_by_changed_protected_files_n=Šis izmaiņu pieprasījums ir bloķēts, jo tas izmaina aizsargātos failus:
pulls.can_auto_merge_desc=Šo izmaiņu pieprasījumu var automātiski sapludināt.
pulls.cannot_auto_merge_desc=Šis izmaiņu pieprasījums nevar tikt automātiski sapludināts konfliktu dēļ.
pulls.cannot_auto_merge_helper=Sapludiniet manuāli, lai atrisinātu konfliktus.
@@ -1667,6 +1765,7 @@ pulls.rebase_conflict_summary=Kļūdas paziņojums
pulls.unrelated_histories=Sapludināšana neizdevās: mērķa un bāzes atzariem nav kopējas vēstures. Ieteikums: izvēlieties citu sapludināšanas stratēģiju
pulls.merge_out_of_date=Sapludināšana neizdevās: sapludināšanas laikā, bāzes atzarā tika iesūtītas izmaiņas. Ieteikums: mēģiniet atkārtoti.
pulls.head_out_of_date=Sapludināšana neizdevās: sapludināšanas laikā, bāzes atzarā tika iesūtītas izmaiņas. Ieteikums: mēģiniet atkārtoti.
+pulls.has_merged=Neizdevās: izmaiņu pieprasījums jau ir sapludināts, nevar to darīt atkārtoti vai mainīt mērķa atzaru.
pulls.push_rejected=Sapludināšana neizdevās: iesūtīšana tika noraidīta. Pārbaudiet git āķus šim repozitorijam.
pulls.push_rejected_summary=Pilns noraidīšanas ziņojums
pulls.push_rejected_no_message=Sapludināšana neizdevās: Izmaiņu iesūtīšana tika noraidīta, bet serveris neatgrieza paziņojumu.
Pārbaudiet git āķus šim repozitorijam
@@ -1678,6 +1777,8 @@ pulls.status_checks_failure=Dažas pārbaudes neizdevās izpildīt
pulls.status_checks_error=Dažu pārbaužu izpildes laikā, radās kļūdas
pulls.status_checks_requested=Obligāts
pulls.status_checks_details=Papildu informācija
+pulls.status_checks_hide_all=Paslēpt visas pārbaudes
+pulls.status_checks_show_all=Parādīt visas pārbaudes
pulls.update_branch=Atjaunot atzaru, izmantojot, sapludināšanu
pulls.update_branch_rebase=Atjaunot atzaru, izmantojot, pārbāzēšanu
pulls.update_branch_success=Atzara atjaunināšana veiksmīgi pabeigta
@@ -1686,6 +1787,11 @@ pulls.outdated_with_base_branch=Atzars ir novecojis salīdzinot ar bāzes atzaru
pulls.close=Aizvērt izmaiņu pieprasījumu
pulls.closed_at=`aizvēra šo izmaiņu pieprasījumu %[2]s`
pulls.reopened_at=`atkārtoti atvēra šo izmaiņu pieprasījumu %[2]s`
+pulls.cmd_instruction_hint=`Apskatīt komandrindas izmantošanas norādes.`
+pulls.cmd_instruction_checkout_title=Paņemt
+pulls.cmd_instruction_checkout_desc=Projekta repozitorijā jāizveido jauns atzars un jāpārbauda izmaiņas.
+pulls.cmd_instruction_merge_title=Sapludināt
+pulls.cmd_instruction_merge_desc=Sapludināt izmaiņas un atjaunot tās Gitea.
pulls.clear_merge_message=Notīrīt sapludināšanas ziņojumu
pulls.clear_merge_message_hint=Notīrot sapludināšanas ziņojumu tiks noņemts tikai pats ziņojums, bet tiks paturēti ģenerētie git ziņojumu, kā "Co-Authored-By …".
@@ -1704,7 +1810,9 @@ pulls.auto_merge_canceled_schedule_comment=`atcēla automātisko sapludināšanu
pulls.delete.title=Dzēst šo izmaiņu pieprasījumu?
pulls.delete.text=Vai patiešām vēlaties dzēst šo izmaiņu pieprasījumu? (Neatgriezeniski tiks izdzēsts viss saturs. Apsveriet iespēju to aizvērt, ja vēlaties informāciju saglabāt vēsturei)
+pulls.recently_pushed_new_branches=Tu iesūtīji izmaiņas atzarā %[1]s %[2]s
+pull.deleted_branch=(izdzēsts):%s
milestones.new=Jauns atskaites punkts
milestones.closed=Aizvērts %s
@@ -1712,6 +1820,7 @@ milestones.update_ago=Atjaunots %s
milestones.no_due_date=Bez termiņa
milestones.open=Atvērta
milestones.close=Aizvērt
+milestones.new_subheader=Atskaites punkti var palīdzēt pārvaldīt problēmas un sekot to virzībai.
milestones.completeness=%d%% pabeigti
milestones.create=Izveidot atskaites punktu
milestones.title=Virsraksts
@@ -1728,12 +1837,25 @@ milestones.edit_success=Izmaiņas atskaites punktā "%s" tika veiksmīgi saglab
milestones.deletion=Dzēst atskaites punktu
milestones.deletion_desc=Dzēšot šo atskaites punktu, tas tiks noņemts no visām saistītajām problēmām un izmaiņu pieprasījumiem. Vai turpināt?
milestones.deletion_success=Atskaites punkts tika veiksmīgi izdzēsts.
+milestones.filter_sort.earliest_due_data=Agrākais izpildes laiks
+milestones.filter_sort.latest_due_date=Vēlākais izpildes laiks
milestones.filter_sort.least_complete=Vismazāk pabeigtais
milestones.filter_sort.most_complete=Visvairāk pabeigtais
milestones.filter_sort.most_issues=Visvairāk problēmu
milestones.filter_sort.least_issues=Vismazāk problēmu
+signing.will_sign=Šī revīzija tiks parakstīta ar atslēgu "%s".
signing.wont_sign.error=Notika kļūda pārbaudot vai revīzija var tikt parakstīta.
+signing.wont_sign.nokey=Nav pieejamas atslēgas, ar ko parakstīt šo revīziju.
+signing.wont_sign.never=Revīzijas nekad netiek parakstītas.
+signing.wont_sign.always=Revīzijas vienmēr tiek parakstītas.
+signing.wont_sign.pubkey=Revīzija netiks parakstīta, jo kontam nav piesaistīta publiskā atslēga.
+signing.wont_sign.twofa=Jābūt iespējotai divfaktoru autentifikācijai, lai parakstītu revīzijas.
+signing.wont_sign.parentsigned=Revīzija netiks parakstīta, jo nav parakstīta vecāka revīzija.
+signing.wont_sign.basesigned=Sapludināšanas revīzija netiks parakstīta, jo pamata revīzija nav parakstīta.
+signing.wont_sign.headsigned=Sapludināšanas revīzija netiks parakstīta, jo galvenā revīzija nav parakstīta.
+signing.wont_sign.commitssigned=Sapludināšana netiks parakstīta, jo visas saistītās revīzijas nav parakstītas.
+signing.wont_sign.approved=Sapludināšana netiks parakstīta, jo izmaiņu pieprasījums nav apstiprināts.
signing.wont_sign.not_signed_in=Jūs neesat pieteicies.
ext_wiki=Piekļuve ārējai vikivietnei
@@ -1832,16 +1954,7 @@ activity.git_stats_and_deletions=un
activity.git_stats_deletion_1=%d dzēšana
activity.git_stats_deletion_n=%d dzēšanas
-search=Meklēt
-search.search_repo=Meklēšana repozitorijā
-search.type.tooltip=Meklēšanas veids
-search.fuzzy=Aptuveni
-search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības
-search.match=Precīzi
-search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības
-search.results=Meklēšanas rezultāti nosacījumam "%s" repozitorijā %s
-search.code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem.
-search.code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru.
+contributors.contribution_type.commits=Revīzijas
settings=Iestatījumi
settings.desc=Iestatījumi ir vieta, kur varat pārvaldīt repozitorija iestatījumus
@@ -1864,6 +1977,7 @@ settings.mirror_settings.docs.disabled_push_mirror.info=Iesūtīšanas spoguļus
settings.mirror_settings.docs.no_new_mirrors=Šis repozitorijs spoguļo izmaiņas uz vai no cita repozitorija. Pašlaik vairāk nav iespējams izveidot jaunus spoguļa repozitorijus.
settings.mirror_settings.docs.can_still_use=Lai arī nav iespējams mainīt esošos vai izveidot jaunus spoguļa repozitorijus, esošie turpinās strādāt.
settings.mirror_settings.docs.pull_mirror_instructions=Lai ietatītu atvilkšanas spoguli, sekojiet instrukcijām:
+settings.mirror_settings.docs.more_information_if_disabled=Vairāk par piegādāšanas un saņemšanas spoguļiem var uzzināt šeit:
settings.mirror_settings.docs.doc_link_title=Kā spoguļot repozitorijus?
settings.mirror_settings.docs.doc_link_pull_section=dokumentācijas nodaļā "Pulling from a remote repository".
settings.mirror_settings.docs.pulling_remote_title=Atvilkt no attāla repozitorija
@@ -1875,8 +1989,11 @@ settings.mirror_settings.last_update=Pēdējās izmaiņas
settings.mirror_settings.push_mirror.none=Nav konfigurēts iesūtīšanas spogulis
settings.mirror_settings.push_mirror.remote_url=Git attālinātā repozitorija URL
settings.mirror_settings.push_mirror.add=Pievienot iesūtīšanas spoguli
+settings.mirror_settings.push_mirror.edit_sync_time=Labot spoguļa sinhronizācijas intervālu
settings.sync_mirror=Sinhronizēt tagad
+settings.pull_mirror_sync_in_progress=Pašlaik tiek saņemtas izmaiņas no attālā %s.
+settings.push_mirror_sync_in_progress=Pašlaik tiek piegādātas izmaiņas uz attālo %s.
settings.site=Mājas lapa
settings.update_settings=Mainīt iestatījumus
settings.update_mirror_settings=Atjaunot spoguļa iestatījumus
@@ -1916,6 +2033,7 @@ settings.pulls.default_allow_edits_from_maintainers=Atļaut uzturētājiem labot
settings.releases_desc=Iespējot repozitorija laidienus
settings.packages_desc=Iespējot repozitorija pakotņu reģistru
settings.projects_desc=Iespējot repozitorija projektus
+settings.projects_mode_all=Visi projekti
settings.actions_desc=Iespējot repozitorija darbības
settings.admin_settings=Administratora iestatījumi
settings.admin_enable_health_check=Iespējot veselības pārbaudi (git fsck) šim repozitorijam
@@ -1943,6 +2061,7 @@ settings.transfer.rejected=Repozitorija īpašnieka maiņas pieprasījums tika n
settings.transfer.success=Repozitorija īpašnieka maiņa veiksmīga.
settings.transfer_abort=Atcelt īpašnieka maiņu
settings.transfer_abort_invalid=Nevar atcelt neeksistējoša repozitorija īpašnieka maiņu.
+settings.transfer_abort_success=Repozitorija īpašnieka maiņa uz %s tika veiksmīgi atcelta.
settings.transfer_desc=Mainīt šī repozitorija īpašnieku uz citu lietotāju vai organizāciju, kurai Jums ir administratora tiesības.
settings.transfer_form_title=Ievadiet repozitorija nosaukumu, lai apstiprinātu:
settings.transfer_in_progress=Pašlaik jau tiek veikta repozitorija īpašnieka maiņa. Atceliet iepriekšējo īpašnieka maiņu, ja vēlaties mainīt uz citu.
@@ -1967,12 +2086,12 @@ settings.trust_model.collaboratorcommitter=Līdzstrādnieka un revīzijas iesūt
settings.trust_model.collaboratorcommitter.long=Līdzstrādnieka un revīzijas iesūtītāja: Uzticēties līdzstrādnieku parakstiem, kas atbilst revīzijas iesūtītājam
settings.trust_model.collaboratorcommitter.desc=Derīgi līdzstrādnieku paraksti tiks atzīmēti kā "uzticami", ja tie atbilst revīzijas iesūtītājam, citos gadījumos tie tiks atzīmēti kā "neuzticami", ja paraksts atbilst revīzijas iesūtītajam, vai "nesakrītoši", ja neatbilst. Šis nozīmē, ka Gitea būs kā revīzijas iesūtītājs parakstītām revīzijām, kur īstais revīzijas iesūtītājs tiks atīzmēts revīzijas komentāra beigās ar tekstu Co-Authored-By: un Co-Committed-By:. Noklusētajai Gitea atslēgai ir jāatbilst lietotājam datubāzē.
settings.wiki_delete=Dzēst vikivietnes datus
-settings.wiki_delete_desc=Vikivietnes repozitorija dzēšana ir NEATGRIEZENISKA. Vai turpināt?
+settings.wiki_delete_desc=Vikivietnes repozitorija dzēšana ir neatgriezeniska un nav atsaucama.
settings.wiki_delete_notices_1=- Šī darbība dzēsīs un atspējos repozitorija %s vikivietni.
settings.confirm_wiki_delete=Dzēst vikivietnes datus
settings.wiki_deletion_success=Repozitorija vikivietnes dati tika izdzēsti.
settings.delete=Dzēst šo repozitoriju
-settings.delete_desc=Repozitorija dzēšana ir NEATGRIEZENISKA. Vai turpināt?
+settings.delete_desc=Repozitorija dzēšana ir neatgriezeniska un nav atsaucama.
settings.delete_notices_1=- Šī darbība ir NEATGRIEZENISKA.
settings.delete_notices_2=- Šī darbība neatgriezeniski izdzēsīs visu repozitorijā %s, tai skaitā problēmas, komentārus, vikivietni un līdzstrādnieku piesaisti.
settings.delete_notices_fork_1=- Visi atdalītie repozitoriju pēc dzēšanas kļūs neatkarīgi.
@@ -1989,7 +2108,6 @@ settings.delete_collaborator=Noņemt
settings.collaborator_deletion=Noņemt līdzstrādnieku
settings.collaborator_deletion_desc=Noņemot līdzstrādnieku, tam tiks liegta piekļuve šim repozitorijam. Vai turpināt?
settings.remove_collaborator_success=Līdzstrādnieks tika noņemts.
-settings.search_user_placeholder=Meklēt lietotāju…
settings.org_not_allowed_to_be_collaborator=Organizācijas nevar tikt pievienotas kā līdzstrādnieki.
settings.change_team_access_not_allowed=Iespēja mainīt komandu piekļuvi repozitorijam ir organizācijas īpašniekam
settings.team_not_in_organization=Komanda nav tajā pašā organizācijā kā repozitorijs
@@ -1997,7 +2115,6 @@ settings.teams=Komandas
settings.add_team=Pievienot komandu
settings.add_team_duplicate=Komandai jau ir piekļuve šim repozitorijam
settings.add_team_success=Komandai tagad ir piekļuve šim repozitorijam.
-settings.search_team=Meklēt komandu…
settings.change_team_permission_tip=Komandas tiesības tiek uzstādītas komandas iestatījumu lapā un nevar tikt individuāli mainītas katram repozitorijam atsevišķi
settings.delete_team_tip=Komandai ir piekļuve visiem repozitorijiem un tā nevar tikt noņemta individuāli
settings.remove_team_success=Komandas piekļuve šim repozitorijam ir noņemta.
@@ -2009,12 +2126,14 @@ settings.webhook_deletion_desc=Noņemot tīmekļa āķi, tiks dzēsti visi tā i
settings.webhook_deletion_success=Tīmekļa āķis tika noņemts.
settings.webhook.test_delivery=Testa piegāde
settings.webhook.test_delivery_desc=Veikt viltus push-notikuma piegādi, lai notestētu Jūsu tīmekļa āķa iestatījumus.
+settings.webhook.test_delivery_desc_disabled=Lai pārbaudītu šo tīmekļa āķi ar neīstu notikumu, tas ir jāiespējo.
settings.webhook.request=Pieprasījums
settings.webhook.response=Atbilde
settings.webhook.headers=Galvenes
settings.webhook.payload=Saturs
settings.webhook.body=Saturs
settings.webhook.replay.description=Izpildīt atkārtoti šo tīmekļa āķi.
+settings.webhook.replay.description_disabled=Lai atkārtoti izpildītu šo tīmekļa āķi, tas ir jāiespējo.
settings.webhook.delivery.success=Notikums tika veiksmīgi pievienots piegādes rindai. Var paiet vairākas sekundes līdz tas parādās piegādes vēsturē.
settings.githooks_desc=Git āķus apstrādā pats Git. Jūs varat labot atbalstīto āku failus sarakstā zemāk, lai veiktu pielāgotas darbības.
settings.githook_edit_desc=Ja āķis nav aktīvs, tiks attēlots piemērs kā to izmantot. Atstājot āķa saturu tukšu, tas tiks atspējots.
@@ -2148,9 +2267,7 @@ settings.protect_whitelist_committers=Atļaut iesūtīt izmaiņas norādītajiem
settings.protect_whitelist_committers_desc=Tikai norādītiem lietotāji vai komandas varēs iesūtīt izmaiņas šajā atzarā (piespiedu izmaiņu iesūtīšanas netiks atļauta).
settings.protect_whitelist_deploy_keys=Atļaut izvietošanas atslēgām ar rakstīšanas tiesībām nosūtīt izmaiņas.
settings.protect_whitelist_users=Lietotāji, kas var veikt izmaiņu nosūtīšanu:
-settings.protect_whitelist_search_users=Meklēt lietotājus…
settings.protect_whitelist_teams=Komandas, kas var veikt izmaiņu nosūtīšanu:
-settings.protect_whitelist_search_teams=Meklēt komandas…
settings.protect_merge_whitelist_committers=Iespējot sapludināšanas ierobežošanu
settings.protect_merge_whitelist_committers_desc=Atļaut tikai noteiktiem lietotājiem vai komandām sapludināt izmaiņu pieprasījumus šajā atzarā.
settings.protect_merge_whitelist_users=Lietotāji, kas var veikt izmaiņu sapludināšanu:
@@ -2174,6 +2291,7 @@ settings.dismiss_stale_approvals_desc=Kad tiek iesūtītas jaunas revīzijas, ka
settings.require_signed_commits=Pieprasīt parakstītas revīzijas
settings.require_signed_commits_desc=Noraidīt iesūtītās izmaiņas šim atzaram, ja tās nav parakstītas vai nav iespējams pārbaudīt.
settings.protect_branch_name_pattern=Aizsargātā zara šablons
+settings.protect_branch_name_pattern_desc=Aizsargāto atzaru nosaukumu šabloni. Šablonu pierakstu skatīt dokumentācijā. Piemēri: main, release/**
settings.protect_patterns=Šabloni
settings.protect_protected_file_patterns=Aizsargāto failu šablons (vairākus var norādīt atdalot ar semikolu ';'):
settings.protect_protected_file_patterns_desc=Aizsargātie faili, ko nevar mainīt, pat ja lietotājam ir tiesības veidot jaunus, labot vai dzēst failus šajā atzarā. Vairākus šablons ir iespējams norādīt atdalot tos ar semikolu (';'). Sīkāka informācija par šabloniem pieejama github.com/gobwas/glob dokumentācijā. Piemēram, .drone.yml
, /docs/**/*.txt
.
@@ -2210,18 +2328,26 @@ settings.tags.protection.allowed.teams=Atļauts komandām
settings.tags.protection.allowed.noone=Nevienam
settings.tags.protection.create=Aizsargāt tagus
settings.tags.protection.none=Nav uzstādīta tagu aizsargāšana.
+settings.tags.protection.pattern.description=Var izmantot vienkāršu nosaukumu vai glob šablonu, vai regulāro izteiksmi, lai atbilstu vairākiem tagiem. Vairāk ir lasāms aizsargāto tagu šablonu dokumentācijā.
settings.bot_token=Bota pilnvara
settings.chat_id=Tērzēšanas ID
+settings.thread_id=Pavediena ID
settings.matrix.homeserver_url=Mājas servera URL
settings.matrix.room_id=Istabas ID
settings.matrix.message_type=Ziņas veids
settings.archive.button=Arhivēt
settings.archive.header=Arhivēt repozitoriju
+settings.archive.text=Repozitorija arhivēšana padarīs to tikai lasāmu. Tas nebūs redzams infopanelī. Neviens nevarēs izveidot jaunas revīzijas vai atvērt jaunus problēmu pieteikumus vai izmaiņu pieprasījumus.
settings.archive.success=Repozitorijs veiksmīgi arhivēts.
settings.archive.error=Arhivējot repozitoriju radās neparedzēta kļūda. Pārbaudiet kļūdu žurnālu, lai uzzinātu sīkāk.
settings.archive.error_ismirror=Nav iespējams arhivēt spoguļotus repozitorijus.
settings.archive.branchsettings_unavailable=Atzaru iestatījumi nav pieejami, ja repozitorijs ir arhivēts.
settings.archive.tagsettings_unavailable=Tagu iestatījumi nav pieejami, ja repozitorijs ir arhivēts.
+settings.unarchive.button=Atcelt repozitorija arhivēšanu
+settings.unarchive.header=Atcelt šī repozitorija arhivēšanu
+settings.unarchive.text=Repozitorija arhivēšanas atcelšana atjaunos tā spēju saņemt izmaiņas, kā arī jaunus problēmu pieteikumus un izmaiņu pieprasījumus.
+settings.unarchive.success=Repozitorijam veiksmīgi atcelta arhivācija.
+settings.unarchive.error=Repozitorija arhivēšanas atcelšanas laikā atgadījās kļūda. Vairāk ir redzams žurnālā.
settings.update_avatar_success=Repozitorija attēls tika atjaunināts.
settings.lfs=LFS
settings.lfs_filelist=LFS faili, kas saglabāti šajā repozitorijā
@@ -2288,6 +2414,7 @@ diff.show_more=Rādīt vairāk
diff.load=Ielādēt izmaiņas
diff.generated=ģenerēts
diff.vendored=ārējs
+diff.comment.add_line_comment=Pievienot rindas komentāru
diff.comment.placeholder=Ievadiet komentāru
diff.comment.markdown_info=Tiek atbalstīta formatēšana ar Markdown.
diff.comment.add_single_comment=Pievienot vienu komentāru
@@ -2344,6 +2471,7 @@ release.edit_release=Labot laidienu
release.delete_release=Dzēst laidienu
release.delete_tag=Dzēst tagu
release.deletion=Dzēst laidienu
+release.deletion_desc=Laidiena izdzēšana tikai noņem to no Gitea. Tā neietekmēs Git tagu, repozitorija saturu vai vēsturi. Vai turpināt?
release.deletion_success=Laidiens tika izdzēsts.
release.deletion_tag_desc=Tiks izdzēsts tags no repozitorija. Repozitorija saturs un vēsture netiks mainīta. Vai turpināt?
release.deletion_tag_success=Tags tika izdzēsts.
@@ -2363,6 +2491,7 @@ branch.already_exists=Atzars ar nosaukumu "%s" jau eksistē.
branch.delete_head=Dzēst
branch.delete=`Dzēst atzaru "%s"`
branch.delete_html=Dzēst atzaru
+branch.delete_desc=Atzara dzēšana ir neatgriezeniska. Kaut arī izdzēstais zars neilgu laiku var turpināt pastāvēt, pirms tas tiešām tiek noņemts, to vairumā gadījumu NEVAR atsaukt. Vai turpināt?
branch.deletion_success=Atzars "%s" tika izdzēsts.
branch.deletion_failed=Neizdevās izdzēst atzaru "%s".
branch.delete_branch_has_new_commits=Atzars "%s" nevar tik dzēsts, jo pēc sapludināšanas, tam ir pievienotas jaunas revīzijas.
@@ -2386,7 +2515,7 @@ branch.create_new_branch=Izveidot jaunu atzaru no atzara:
branch.confirm_create_branch=Izveidot atzaru
branch.warning_rename_default_branch=Tiks pārsaukts noklusētais atzars.
branch.rename_branch_to=Pārsaukt "%s" uz:
-branch.confirm_rename_branch=Pārsaukt atzaru
+branch.confirm_rename_branch=Pārdēvēt atzaru
branch.create_branch_operation=Izveidot atzaru
branch.new_branch=Izveidot jaunu atzaru
branch.new_branch_from=`Izveidot jaunu atzaru no "%s"`
@@ -2402,6 +2531,7 @@ tag.create_success=Tags "%s" tika izveidots.
topic.manage_topics=Pārvaldīt tēmas
topic.done=Gatavs
topic.count_prompt=Nevar pievienot vairāk kā 25 tēmas
+topic.format_prompt=Tēmai jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un punktus ('.') un var būt līdz 35 rakstzīmēm gara. Burtiem jābūt mazajiem.
find_file.go_to_file=Iet uz failu
find_file.no_matching=Atbilstošs fails netika atrasts
@@ -2410,6 +2540,12 @@ error.csv.too_large=Nevar attēlot šo failu, jo tas ir pārāk liels.
error.csv.unexpected=Nevar attēlot šo failu, jo tas satur neparedzētu simbolu %d. līnijas %d. kolonnā.
error.csv.invalid_field_count=Nevar attēlot šo failu, jo tas satur nepareizu skaitu ar laukiem %d. līnijā.
+[graphs]
+component_loading=Ielādē %s...
+component_loading_failed=Nevarēja ielādēt %s
+component_loading_info=Šis var aizņemt kādu brīdi…
+component_failed_to_load=Atgadījās neparedzēta kļūda.
+
[org]
org_name_holder=Organizācijas nosaukums
org_full_name_holder=Organizācijas pilnais nosaukums
@@ -2440,6 +2576,7 @@ form.create_org_not_allowed=Jums nav tiesību veidot jauno organizāciju.
settings=Iestatījumi
settings.options=Organizācija
settings.full_name=Pilns vārds, uzvārds
+settings.email=E-pasta adrese saziņai
settings.website=Mājas lapa
settings.location=Atrašanās vieta
settings.permission=Tiesības
@@ -2453,6 +2590,7 @@ settings.visibility.private_shortname=Privāta
settings.update_settings=Mainīt iestatījumus
settings.update_setting_success=Organizācijas iestatījumi tika saglabāti.
+settings.change_orgname_prompt=Piezīme: organizācijas nosaukuma maiņa izmainīs arī organizācijas URL un atbrīvos veco nosaukumu.
settings.change_orgname_redirect_prompt=Vecais vārds pārsūtīs uz jauno, kamēr vien tas nebūs izmantots.
settings.update_avatar_success=Organizācijas attēls tika saglabāts.
settings.delete=Dzēst organizāciju
@@ -2472,7 +2610,7 @@ members.private=Slēpts
members.private_helper=padarīt redzemu
members.member_role=Dalībnieka loma:
members.owner=Īpašnieks
-members.member=Biedri
+members.member=Dalībnieks
members.remove=Noņemt
members.remove.detail=Noņemt lietotāju %[1]s no organizācijas %[2]s?
members.leave=Atstāt
@@ -2512,7 +2650,6 @@ teams.write_permission_desc=Šai komandai ir rakstīšanas ties
teams.admin_permission_desc=Šai komandai ir administratora tiesības: dalībnieki var lasīt, rakstīt un pievienot citus dalībniekus komandas repozitorijiem.
teams.create_repo_permission_desc=Papildus šī komanda piešķirt Veidot repozitorijus tiesības: komandas biedri var veidot jaunus repozitorijus šajā organizācijā.
teams.repositories=Komandas repozitoriji
-teams.search_repo_placeholder=Meklēt repozitorijā…
teams.remove_all_repos_title=Noņemt visus komandas repozitorijus
teams.remove_all_repos_desc=Šī darbība noņems visus repozitorijus no komandas.
teams.add_all_repos_title=Pievienot visus repozitorijus
@@ -2528,27 +2665,34 @@ teams.all_repositories_helper=Šai komandai ir piekļuve visiem repozitorijiem.
teams.all_repositories_read_permission_desc=Šī komanda piešķirt skatīšanās tiesības visiem repozitorijiem: komandas biedri var skatīties un klonēt visus organizācijas repozitorijus.
teams.all_repositories_write_permission_desc=Šī komanda piešķirt labošanas tiesības visiem repozitorijiem: komandas biedri var skatīties un nosūtīt izmaiņas visiem organizācijas repozitorijiem.
teams.all_repositories_admin_permission_desc=Šī komanda piešķirt administratora tiesības visiem repozitorijiem: komandas biedri var skatīties, nosūtīt izmaiņas un mainīt iestatījumus visiem organizācijas repozitorijiem.
+teams.invite.title=Tu esi uzaicināts pievienoties organizācijas %[2]s komandai %[1]s.
teams.invite.by=Uzaicināja %s
teams.invite.description=Nospiediet pogu zemāk, lai pievienotos komandai.
[admin]
dashboard=Infopanelis
+self_check=Pašpārbaude
+identity_access=Identitāte un piekļuve
users=Lietotāju konti
organizations=Organizācijas
+assets=Koda aktīvi
repositories=Repozitoriji
hooks=Tīmekļa āķi
+integrations=Integrācijas
authentication=Autentificēšanas avoti
emails=Lietotāja e-pasts
config=Konfigurācija
+config_summary=Kopsavilkums
+config_settings=Iestatījumi
notices=Sistēmas paziņojumi
monitor=Uzraudzība
first_page=Pirmā
last_page=Pēdējā
total=Kopā: %d
+settings=Administratora iestatījumi
dashboard.new_version_hint=Ir pieejama Gitea versija %s, pašreizējā versija %s. Papildus informācija par jauno versiju ir pieejama mājas lapā.
dashboard.statistic=Kopsavilkums
-dashboard.operations=Uzturēšanas darbības
dashboard.system_status=Sistēmas statuss
dashboard.operation_name=Darbības nosaukums
dashboard.operation_switch=Pārslēgt
@@ -2557,11 +2701,13 @@ dashboard.clean_unbind_oauth=Notīrīt nepiesaistītos OAuth savienojumus
dashboard.clean_unbind_oauth_success=Visi nepiesaistītie OAuth savienojumu tika izdzēsti.
dashboard.task.started=Uzsākts uzdevums: %[1]s
dashboard.task.process=Uzdevums: %[1]s
+dashboard.task.cancelled=Uzdevums: %[1]s atcelts: %[3]s
dashboard.task.error=Kļūda uzdevuma izpildē: %[1]s: %[3]s
dashboard.task.finished=Uzdevums: %[1]s, ko iniciēja %[2]s ir izpildīts
dashboard.task.unknown=Nezināms uzdevums: %[1]s
dashboard.cron.started=Uzsākts Cron: %[1]s
dashboard.cron.process=Cron: %[1]s
+dashboard.cron.cancelled=Cron: %[1]s atcelts: %[3]s
dashboard.cron.error=Kļūda Cron: %s: %[3]s
dashboard.cron.finished=Cron: %[1]s pabeigts
dashboard.delete_inactive_accounts=Dzēst visus neaktivizētos kontus
@@ -2571,6 +2717,7 @@ dashboard.delete_repo_archives.started=Uzdevums visu repozitoriju arhīvu dzēš
dashboard.delete_missing_repos=Dzēst visus repozitorijus, kam trūkst Git failu
dashboard.delete_missing_repos.started=Uzdevums visu repozitoriju dzēšanai, kam trūkst git failu, uzsākts.
dashboard.delete_generated_repository_avatars=Dzēst ģenerētos repozitoriju attēlus
+dashboard.sync_repo_branches=Sinhronizācija ar dabubāzi izlaida atzarus no git datiem
dashboard.update_mirrors=Atjaunot spoguļus
dashboard.repo_health_check=Pārbaudīt visu repozitoriju veselību
dashboard.check_repo_stats=Pārbaudīt visu repozitoriju statistiku
@@ -2585,6 +2732,7 @@ dashboard.reinit_missing_repos=Atkārtoti inicializēt visus pazaudētos Git rep
dashboard.sync_external_users=Sinhronizēt ārējo lietotāju datus
dashboard.cleanup_hook_task_table=Iztīrīt tīmekļa āķu vēsturi
dashboard.cleanup_packages=Notīrīt novecojušās pakotnes
+dashboard.cleanup_actions=Notīrīt darbību izbeigušos žurnālus un artefaktus
dashboard.server_uptime=Servera darbības laiks
dashboard.current_goroutine=Izmantotās Gorutīnas
dashboard.current_memory_usage=Pašreiz izmantotā atmiņa
@@ -2622,6 +2770,9 @@ dashboard.gc_lfs=Veikt atkritumu uzkopšanas darbus LFS meta objektiem
dashboard.stop_zombie_tasks=Apturēt zombija uzdevumus
dashboard.stop_endless_tasks=Apturēt nepārtrauktus uzdevumus
dashboard.cancel_abandoned_jobs=Atcelt pamestus darbus
+dashboard.start_schedule_tasks=Sākt plānotos uzdevumus
+dashboard.sync_branch.started=Sākta atzaru sinhronizācija
+dashboard.rebuild_issue_indexer=Pārbūvēt problēmu indeksu
users.user_manage_panel=Lietotāju kontu pārvaldība
users.new_account=Izveidot lietotāja kontu
@@ -2630,6 +2781,9 @@ users.full_name=Vārds, uzvārds
users.activated=Aktivizēts
users.admin=Administrators
users.restricted=Ierobežots
+users.reserved=Aizņemts
+users.bot=Bots
+users.remote=Attāls
users.2fa=2FA
users.repos=Repozitoriji
users.created=Izveidots
@@ -2676,6 +2830,7 @@ users.list_status_filter.is_prohibit_login=Nav atļauta autorizēšanās
users.list_status_filter.not_prohibit_login=Atļaut autorizāciju
users.list_status_filter.is_2fa_enabled=2FA iespējots
users.list_status_filter.not_2fa_enabled=2FA nav iespējots
+users.details=Lietotāja informācija
emails.email_manage_panel=Lietotāju e-pastu pārvaldība
emails.primary=Primārais
@@ -2688,6 +2843,7 @@ emails.updated=E-pasts atjaunots
emails.not_updated=Neizdevās atjaunot pieprasīto e-pasta adresi: %v
emails.duplicate_active=E-pasta adrese jau ir aktīva citam lietotājam.
emails.change_email_header=Atjaunot e-pasta rekvizītus
+emails.change_email_text=Vai patiešām vēlaties atjaunot šo e-pasta adresi?
orgs.org_manage_panel=Organizāciju pārvaldība
orgs.name=Nosaukums
@@ -2701,15 +2857,15 @@ repos.unadopted.no_more=Netika atrasts neviens nepārņemtais repozitorijs
repos.owner=Īpašnieks
repos.name=Nosaukums
repos.private=Privāts
-repos.watches=Vērošana
-repos.stars=Atzīmētās zvaigznītes
-repos.forks=Atdalītie
repos.issues=Problēmas
repos.size=Izmērs
+repos.lfs_size=LFS izmērs
packages.package_manage_panel=Pakotņu pārvaldība
packages.total_size=Kopējais izmērs: %s
packages.unreferenced_size=Izmērs bez atsauces: %s
+packages.cleanup=Notīrīt novecojušos datus
+packages.cleanup.success=Novecojuši dati veiksmīgi notīrīti
packages.owner=Īpašnieks
packages.creator=Izveidotājs
packages.name=Nosaukums
@@ -2720,10 +2876,12 @@ packages.size=Izmērs
packages.published=Publicēts
defaulthooks=Noklusētie tīmekļa āķi
+defaulthooks.desc=Tīmekļa āķi automātiski nosūta HTTP POST pieprasījumus serverim, kad iestājas noteikti Gitea notikumi. Šeit pievienotie tīmekļa āķi ir noklusējuma, un tie tiks pievienoti visiem jaunajiem repozitorijiem. Vairāk ir lasāms tīmekļa āķu dokumentācijā.
defaulthooks.add_webhook=Pievienot noklusēto tīmekļa āķi
defaulthooks.update_webhook=Mainīt noklusēto tīmekļa āķi
systemhooks=Sistēmas tīmekļa āķi
+systemhooks.desc=Tīmekļa āķi automātiski nosūta HTTP POST pieprasījumus serverim, kad iestājas noteikti Gitea notikumi. Šeit pievienotie tīmekļa āķi tiks izsaukti visiem sistēmas repozitorijiem, tādēļ lūgums apsvērt to iespējamo ietekmi uz veiktspēju. Vairāk ir lasāms tīmekļa āķu dokumentācijā.
systemhooks.add_webhook=Pievienot sistēmas tīmekļa āķi
systemhooks.update_webhook=Mainīt sistēmas tīmekļa āķi
@@ -2816,17 +2974,18 @@ auths.sspi_default_language=Noklusētā lietotāja valoda
auths.sspi_default_language_helper=Noklusētā valoda, ko uzstādīt automātiski izveidotajiem lietotājiem, kas izmanto SSPI autentifikācijas veidu. Atstājiet tukšu, ja vēlaties, lai valoda tiktu noteikta automātiski.
auths.tips=Padomi
auths.tips.oauth2.general=OAuth2 autentifikācija
+auths.tips.oauth2.general.tip=Kad tiek reģistrēta jauna OAuth2 autentifikācija, atzvanīšanas/pārvirzīšanas URL vajadzētu būt:
auths.tip.oauth2_provider=OAuth2 pakalpojuma sniedzējs
auths.tip.bitbucket=Reģistrējiet jaunu OAuth klientu adresē https://bitbucket.org/account/user//oauth-consumers/new un piešķiriet tam "Account" - "Read" tiesības
auths.tip.nextcloud=`Reģistrējiet jaunu OAuth klientu jūsu instances sadāļā "Settings -> Security -> OAuth 2.0 client"`
auths.tip.dropbox=Izveidojiet jaunu aplikāciju adresē https://www.dropbox.com/developers/apps
auths.tip.facebook=`Reģistrējiet jaunu aplikāciju adresē https://developers.facebook.com/apps un pievienojiet produktu "Facebook Login"`
auths.tip.github=Reģistrējiet jaunu aplikāciju adresē https://github.com/settings/applications/new
-auths.tip.gitlab=Reģistrējiet jaunu aplikāciju adresē https://gitlab.com/profile/applications
auths.tip.google_plus=Iegūstiet OAuth2 klienta pilnvaru no Google API konsoles adresē https://console.developers.google.com/
auths.tip.openid_connect=Izmantojiet OpenID pieslēgšanās atklāšanas URL (/.well-known/openid-configuration), lai norādītu galapunktus
auths.tip.twitter=Dodieties uz adresi https://dev.twitter.com/apps, izveidojiet lietotni un pārliecinieties, ka ir atzīmēts “Allow this application to be used to Sign in with Twitter”
auths.tip.discord=Reģistrējiet jaunu aplikāciju adresē https://discordapp.com/developers/applications/me
+auths.tip.gitea=Pievienot jaunu OAuth2 lietojumprogrammu. Dokumentācija ir pieejama https://docs.gitea.com/development/oauth2-provider
auths.tip.yandex=`Izveidojiet jaunu lietotni adresē https://oauth.yandex.com/client/new. Izvēlieties sekojošas tiesības "Yandex.Passport API" sadaļā: "Access to email address", "Access to user avatar" un "Access to username, first name and surname, gender"`
auths.tip.mastodon=Norādiet pielāgotu mastodon instances URL, ar kuru vēlaties autorizēties (vai izmantojiet noklusēto)
auths.edit=Labot autentifikācijas avotu
@@ -2856,6 +3015,7 @@ config.disable_router_log=Atspējot maršrutētāja žurnalizēšanu
config.run_user=Izpildes lietotājs
config.run_mode=Izpildes režīms
config.git_version=Git versija
+config.app_data_path=Lietotnes datu ceļš
config.repo_root_path=Repozitoriju glabāšanas vieta
config.lfs_root_path=LFS saknes ceļš
config.log_file_root_path=Žurnalizēšanas ceļš
@@ -3005,8 +3165,10 @@ monitor.queue.name=Nosaukums
monitor.queue.type=Veids
monitor.queue.exemplar=Eksemplāra veids
monitor.queue.numberworkers=Strādņu skaits
+monitor.queue.activeworkers=Darbojošies strādņi
monitor.queue.maxnumberworkers=Maksimālais strādņu skaits
monitor.queue.numberinqueue=Skaits rindā
+monitor.queue.review_add=Pārskatīt/pievienot strādņus
monitor.queue.settings.title=Pūla iestatījumi
monitor.queue.settings.desc=Pūls dinamiski tiek palielināts atkarībā no bloķētiem darbiem rindā.
monitor.queue.settings.maxnumberworkers=Maksimālais strādņu skaits
@@ -3032,6 +3194,8 @@ notices.desc=Apraksts
notices.op=Op.
notices.delete_success=Sistēmas paziņojumi ir dzēsti.
+self_check.no_problem_found=Pašlaik nav atrasta neviena problēma.
+
[action]
create_repo=izveidoja repozitoriju %s
rename_repo=pārsauca repozitoriju no %[1]s
uz %[3]s
@@ -3062,7 +3226,7 @@ publish_release=`izveidoja versiju "%[4]s" repozitorijā <
review_dismissed=`noraidīja lietotāja %[4]s recenziju izmaiņu pieprasījumam %[3]s#%[2]s`
review_dismissed_reason=Iemesls:
create_branch=izveidoja atzaru %[3]s repozitorijā %[4]s
-starred_repo=atzīmēja ar zvaigznīti %[2]s
+starred_repo=pievienoja izlasē %[2]s
watched_repo=sāka sekot %[2]s
[tool]
@@ -3127,6 +3291,7 @@ desc=Pārvaldīt repozitorija pakotnes.
empty=Pašlaik šeit nav nevienas pakotnes.
empty.documentation=Papildus informācija par pakotņu reģistru pieejama dokumentācijā.
empty.repo=Neparādās augšupielādēta pakotne? Apmeklējiet pakotņu iestatījumus, lai sasaistītu ar repozitoriju.
+registry.documentation=Vairāk informācija par %s reģistru ir pieejama dokumentācijā.
filter.type=Veids
filter.type.all=Visas
filter.no_result=Pēc norādītajiem kritērijiem nekas netika atrasts.
@@ -3212,7 +3377,11 @@ pub.install=Lai instalētu Dart pakotni, izpildiet sekojošu komandu:
pypi.requires=Nepieciešams Python
pypi.install=Lai instalētu pip pakotni, izpildiet sekojošu komandu:
rpm.registry=Konfigurējiet šo reģistru no komandrindas:
+rpm.distros.redhat=uz RedHat balstītās operētājsistēmās
+rpm.distros.suse=uz SUSE balstītās operētājsistēmās
rpm.install=Lai uzstādītu pakotni, ir jāizpilda šī komanda:
+rpm.repository=Repozitorija informācija
+rpm.repository.architectures=Arhitektūras
rubygems.install=Lai instalētu gem pakotni, izpildiet sekojošu komandu:
rubygems.install2=vai pievienojiet Gemfile:
rubygems.dependencies.runtime=Izpildlaika atkarības
@@ -3236,14 +3405,17 @@ settings.delete.success=Pakotne tika izdzēsta.
settings.delete.error=Neizdevās izdzēst pakotni.
owner.settings.cargo.title=Cargo reģistra inkdess
owner.settings.cargo.initialize=Inicializēt indeksu
+owner.settings.cargo.initialize.description=Ir nepieciešams īpašs indeksa Git repozitorijs, lai izmantotu Cargo reģistru. Šīs iespējas izmantošana (atkārtoti) izveidos repozitoriju un automātiski to iestatīs.
owner.settings.cargo.initialize.error=Neizdevās inicializēt Cargo indeksu: %v
owner.settings.cargo.initialize.success=Cargo indekss tika veiksmīgi inicializēts.
owner.settings.cargo.rebuild=Pārbūvēt indeksu
+owner.settings.cargo.rebuild.description=Pārbūvēšana var būt noderīga, ja indekss nav sinhronizēts ar saglabātajām Cargo pakotnēm.
owner.settings.cargo.rebuild.error=Neizdevās pārbūvēt Cargo indeksu: %v
owner.settings.cargo.rebuild.success=Cargo indekss tika veiksmīgi pārbūvēts.
owner.settings.cleanuprules.title=Pārvaldīt notīrīšanas noteikumus
owner.settings.cleanuprules.add=Pievienot notīrīšanas noteikumu
owner.settings.cleanuprules.edit=Labot notīrīšanas noteikumu
+owner.settings.cleanuprules.none=Nav pievienoti tīrīšanas noteikumi. Sīkāku informāciju iespējams iegūt dokumentācijā.
owner.settings.cleanuprules.preview=Notīrīšānas noteikuma priekšskatījums
owner.settings.cleanuprules.preview.overview=Ir ieplānota %d paku dzēšana.
owner.settings.cleanuprules.preview.none=Notīrīšanas noteikumam neatbilst neviena pakotne.
@@ -3262,6 +3434,7 @@ owner.settings.cleanuprules.success.update=Notīrīšanas noteikumi tika atjauno
owner.settings.cleanuprules.success.delete=Notīrīšanas noteikumi tika izdzēsti.
owner.settings.chef.title=Chef reģistrs
owner.settings.chef.keypair=Ģenerēt atslēgu pāri
+owner.settings.chef.keypair.description=Atslēgu pāris ir nepieciešams, lai autentificētos Chef reģistrā. Ja iepriekš ir izveidots atslēgu pāris, jauna pāra izveidošana veco atslēgu pāri padarīs nederīgu.
[secrets]
secrets=Noslēpumi
@@ -3288,6 +3461,7 @@ status.waiting=Gaida
status.running=Izpildās
status.success=Pabeigts
status.failure=Neveiksmīgs
+status.cancelled=Atcelts
status.skipped=Izlaists
status.blocked=Bloķēts
@@ -3300,11 +3474,12 @@ runners.id=ID
runners.name=Nosaukums
runners.owner_type=Veids
runners.description=Apraksts
-runners.labels=Etiķetes
+runners.labels=Iezīmes
runners.last_online=Pēdējo reizi tiešsaistē
runners.runner_title=Izpildītājs
runners.task_list=Pēdējās darbības, kas izpildītas
-runners.task_list.run=Palaist
+runners.task_list.no_tasks=Vēl nav uzdevumu.
+runners.task_list.run=Izpildīt
runners.task_list.status=Statuss
runners.task_list.repository=Repozitorijs
runners.task_list.commit=Revīzija
@@ -3324,16 +3499,47 @@ runners.status.idle=Dīkstāvē
runners.status.active=Aktīvs
runners.status.offline=Bezsaistē
runners.version=Versija
+runners.reset_registration_token=Atiestatīt reģistrācijas pilnvaru
runners.reset_registration_token_success=Izpildītāja reģistrācijas pilnvara tika veiksmīgi atiestatīta
runs.all_workflows=Visas darbaplūsmas
runs.commit=Revīzija
+runs.scheduled=Ieplānots
+runs.pushed_by=iesūtīja
runs.invalid_workflow_helper=Darbaplūsmas konfigurācijas fails ir kļūdains. Pārbaudiet konfiugrācijas failu: %s
+runs.no_matching_online_runner_helper=Nav pieejami izpildītāji, kas atbilstu šai iezīmei: %s
+runs.actor=Aktors
runs.status=Statuss
+runs.actors_no_select=Visi aktori
+runs.status_no_select=Visi stāvokļi
+runs.no_results=Netika atrasts nekas atbilstošs.
+runs.no_workflows=Vēl nav nevienas darbplūsmas.
+runs.no_runs=Darbplūsmai vēl nav nevienas izpildes.
+runs.empty_commit_message=(tukšs revīzijas ziņojums)
+workflow.disable=Atspējot darbplūsmu
+workflow.disable_success=Darbplūsma '%s' ir veiksmīgi atspējota.
+workflow.enable=Iespējot darbplūsmu
+workflow.enable_success=Darbplūsma '%s' ir veiksmīgi iespējota.
+workflow.disabled=Darbplūsma ir atspējota.
need_approval_desc=Nepieciešams apstiprinājums, lai izpildītu izmaiņu pieprasījumu darbaplūsmas no atdalītiem repozitorijiem.
+variables=Mainīgie
+variables.management=Mainīgo pārvaldība
+variables.creation=Pievienot mainīgo
+variables.none=Vēl nav neviena mainīgā.
+variables.deletion=Noņemt mainīgo
+variables.deletion.description=Mainīgā noņemšana ir neatgriezeniska un nav atsaucama. Vai turpināt?
+variables.description=Mainīgie tiks padoti noteiktām darbībām, un citādāk tos nevar nolasīt.
+variables.id_not_exist=Mainīgais ar identifikatoru %d nepastāv.
+variables.edit=Labot mainīgo
+variables.deletion.failed=Neizdevās noņemt mainīgo.
+variables.deletion.success=Mainīgais tika noņemts.
+variables.creation.failed=Neizdevās pievienot mainīgo.
+variables.creation.success=Mainīgais "%s" tika pievienots.
+variables.update.failed=Neizdevās labot mainīgo.
+variables.update.success=Mainīgais tika labots.
[projects]
type-1.display_name=Individuālais projekts
@@ -3341,6 +3547,11 @@ type-2.display_name=Repozitorija projekts
type-3.display_name=Organizācijas projekts
[git.filemode]
+changed_filemode=%[1]s → %[2]s
; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Direktorija
+normal_file=Parasts fails
+executable_file=Izpildāmais fails
symbolic_link=Simboliska saite
+submodule=Apakšmodulis
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 43265c9c31..6b5122a86f 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -115,6 +115,15 @@ concept_user_organization=Organisatie
name=Naam
+filter=Filter
+filter.is_archived=Gearchiveerd
+filter.is_template=Sjabloon
+filter.public=Publiek
+filter.private=Prive
+
+
+[search]
+
[aria]
[heatmap]
@@ -253,7 +262,6 @@ collaborative_repos=Gedeelde repositories
my_orgs=Mijn organisaties
my_mirrors=Mijn spiegels
view_home=Bekijk %s
-search_repos=Zoek een repository…
filter=Andere filters
filter_by_team_repositories=Filter op team repositories
feed_of=`Feed van "%s"`
@@ -274,15 +282,7 @@ issues.in_your_repos=In uw repositories
repos=Repositories
users=Gebruikers
organizations=Organisaties
-search=Zoeken
code=Code
-search.fuzzy=Vergelijkbaar
-search.match=Overeenkomst
-code_search_unavailable=Er is momenteel geen code zoekfunctie beschikbaar. Neem contact op met uw sitebeheerder.
-repo_no_results=Er zijn geen overeenkomende repositories gevonden.
-user_no_results=Er zijn geen overeenkomende gebruikers gevonden.
-org_no_results=Er zijn geen overeenkomende organisaties gevonden.
-code_no_results=Geen broncode gevonden in overeenstemming met uw zoekterm.
code_last_indexed_at=Laatst geïndexeerd %s
[auth]
@@ -296,7 +296,6 @@ remember_me=Onthoud dit apparaat
forgot_password_title=Wachtwoord vergeten
forgot_password=Wachtwoord vergeten?
sign_up_now=Een account nodig? Meld u nu aan.
-confirmation_mail_sent_prompt=Een nieuwe bevestigingsmail is gestuurd naar %s. De mail moet binnen %s worden bevestigd om je registratie te voltooien.
must_change_password=Uw wachtwoord wijzigen
allow_password_change=Verplicht de gebruiker om zijn/haar wachtwoord te wijzigen (aanbevolen)
reset_password_mail_sent_prompt=Een bevestigingsmail is verstuurd naar %s. Controleer uw inbox in de volgende %s om het herstel van uw account te voltooien.
@@ -489,6 +488,7 @@ auth_failed=Verificatie mislukt: %v
target_branch_not_exist=Doel branch bestaat niet
+
[user]
change_avatar=Wijzig je profielfoto…
repositories=repositories
@@ -505,6 +505,7 @@ user_bio=Biografie
disabled_public_activity=Deze gebruiker heeft de publieke zichtbaarheid van de activiteit uitgeschakeld.
+
[settings]
profile=Profiel
account=Account
@@ -632,7 +633,6 @@ gpg_invalid_token_signature=De opgegeven GPG-sleutel, handtekening en token kome
gpg_token_required=U moet een handtekening opgeven voor de onderstaande token
gpg_token=Token
gpg_token_help=U kunt een handtekening genereren met:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Gepantserde GPG-handtekening
key_signature_gpg_placeholder=Begint met '-----BEGIN PGP SIGNATURE-----'
ssh_key_verified=Geverifieerde sleutel
@@ -784,7 +784,6 @@ already_forked=Je hebt %s al geforked
fork_to_different_account=Fork naar een ander account
fork_visibility_helper=De zichtbaarheid van een geforkte repository kan niet worden veranderd.
use_template=Gebruik dit sjabloon
-clone_in_vsc=Kloon in VS Code
download_zip=ZIP downloaden
download_tar=TAR.GZ downloaden
download_bundle=BUNDLE downloaden
@@ -1048,8 +1047,6 @@ editor.revert=%s ongedaan maken op:
commits.desc=Bekijk de broncode-wijzigingsgeschiedenis.
commits.commits=Commits
commits.nothing_to_compare=Deze branches zijn gelijk.
-commits.search=Zoek commits…
-commits.find=Zoek
commits.search_all=Alle branches
commits.author=Auteur
commits.message=Bericht
@@ -1094,7 +1091,6 @@ projects.type.basic_kanban=Basis Kanban
projects.type.bug_triage=Bug Triage
projects.template.desc=Project sjabloon
projects.template.desc_helper=Selecteer een projecttemplate om aan de slag te gaan
-projects.type.uncategorized=Ongecategoriseerd
projects.column.edit_title=Naam
projects.column.new_title=Naam
projects.column.color=Kleur
@@ -1405,7 +1401,6 @@ pulls.compare_compare=trekken van
pulls.switch_comparison_type=Wissel vergelijking type
pulls.switch_head_and_base=Verwissel hoofd en basis
pulls.filter_branch=Filter branch
-pulls.no_results=Geen resultaten gevonden.
pulls.nothing_to_compare=Deze branches zijn gelijk. Er is geen pull-aanvraag nodig.
pulls.nothing_to_compare_and_allow_empty_pr=Deze branches zijn gelijk. Deze pull verzoek zal leeg zijn.
pulls.has_pull_request=`Een pull-verzoek tussen deze branches bestaat al: %[2]s#%[3]d`
@@ -1618,13 +1613,7 @@ activity.git_stats_and_deletions=en
activity.git_stats_deletion_1=%d verwijdering
activity.git_stats_deletion_n=%d verwijderingen
-search=Zoek
-search.search_repo=Zoek repository
-search.fuzzy=Vergelijkbaar
-search.match=Overeenkomst
-search.results=Zoek resultaat voor "%s" in %s
-search.code_no_results=Geen broncode gevonden die aan uw zoekterm voldoet.
-search.code_search_unavailable=Er is momenteel geen code zoekfunctie beschikbaar. Neem contact op met uw sitebeheerder.
+contributors.contribution_type.commits=Commits
settings=Instellingen
settings.desc=In de instellingen kan je de instellingen van de repository aanpassen
@@ -1702,7 +1691,6 @@ settings.delete_collaborator=Verwijder
settings.collaborator_deletion=Verwijder medewerker
settings.collaborator_deletion_desc=Het verwijderen van een collaborator zal hun toegang tot deze repository intrekken. Doorgaan?
settings.remove_collaborator_success=De medewerker is verwijderd.
-settings.search_user_placeholder=Zoek gebruiker…
settings.org_not_allowed_to_be_collaborator=Organisaties kunnen niet worden toegevoegd als een medewerker.
settings.change_team_access_not_allowed=Het veranderen van team toegang voor de repository is beperkt tot de organisatie eigenaar
settings.team_not_in_organization=Het team zit niet in dezelfde organisatie als de repository
@@ -1710,7 +1698,6 @@ settings.teams=Teams
settings.add_team=Team toevoegen
settings.add_team_duplicate=Team heeft al de repository
settings.add_team_success=Het team heeft nu toegang tot de repository.
-settings.search_team=Zoek team…
settings.change_team_permission_tip=Teammachtiging is ingesteld op de team-instellingspagina en kan niet per repository worden gewijzigd
settings.delete_team_tip=Dit team heeft toegang tot alle repositories en kan niet verwijderd worden
settings.remove_team_success=De toegang van het team tot de repository is verwijderd.
@@ -1842,9 +1829,7 @@ settings.protect_whitelist_committers=Whitelist Beperkte Push
settings.protect_whitelist_committers_desc=Alleen gewhiteliste gebruikers of teams mogen pushen naar deze branch (maar geen force push).
settings.protect_whitelist_deploy_keys=Whitelist deploy sleutels met schrijftoegang om te pushen.
settings.protect_whitelist_users=Toegestane gebruikers voor push:
-settings.protect_whitelist_search_users=Zoek gebruiker…
settings.protect_whitelist_teams=Toegestane teams voor push:
-settings.protect_whitelist_search_teams=Zoek teams…
settings.protect_merge_whitelist_committers=Samenvoegen whitelist inschakelen
settings.protect_merge_whitelist_committers_desc=Sta alleen gebruikers of teams van de whitelist toe om pull requests samen te voegen met deze branch.
settings.protect_merge_whitelist_users=Toegestane gebruikers voor samenvoegen:
@@ -2031,6 +2016,8 @@ topic.count_prompt=Je kunt niet meer dan 25 onderwerpen selecteren
+[graphs]
+
[org]
org_name_holder=Organisatienaam
org_full_name_holder=Volledige naam organisatie
@@ -2118,7 +2105,6 @@ teams.write_permission_desc=Dit team heeft Schrijf rechten: led
teams.admin_permission_desc=Dit team heeft beheersrechten: leden kunnen van en naar teamrepositories pullen, pushen, en er medewerkers aan toevoegen.
teams.create_repo_permission_desc=Daarnaast verleent dit team Maak repository permissie: leden kunnen nieuwe repositories maken in de organisatie.
teams.repositories=Teamrepositories
-teams.search_repo_placeholder=Repository zoeken…
teams.remove_all_repos_title=Verwijder alle team repositories
teams.remove_all_repos_desc=Dit zal alle repositories uit het team verwijderen.
teams.add_all_repos_title=Voeg alle repositories toe
@@ -2140,6 +2126,8 @@ repositories=Repositories
authentication=Authenticatie bronnen
emails=Gebruikeremails
config=Configuratie
+config_summary=Overzicht
+config_settings=Instellingen
notices=Systeem aankondigingen
monitor=Bijhouden
first_page=Eerste
@@ -2147,7 +2135,6 @@ last_page=Laatste
total=Totaal: %d
dashboard.statistic=Overzicht
-dashboard.operations=Onderhoudswerkzaamheden
dashboard.system_status=Systeemtatus
dashboard.operation_name=Bewerking naam
dashboard.operation_switch=Omschakelen
@@ -2277,9 +2264,6 @@ repos.unadopted.no_more=Geen niet-geadopteerde repositories meer gevonden
repos.owner=Eigenaar
repos.name=Naam
repos.private=Prive
-repos.watches=Volgers
-repos.stars=Sterren
-repos.forks=Forks
repos.issues=Kwesties
repos.size=Grootte
@@ -2360,7 +2344,6 @@ auths.tip.nextcloud=`Registreer een nieuwe OAuth consument op je installatie met
auths.tip.dropbox=Maak een nieuwe applicatie aan op https://www.dropbox.com/developers/apps
auths.tip.facebook=Registreer een nieuwe applicatie op https://developers.facebook.com/apps en voeg het product "Facebook Login" toe
auths.tip.github=Registreer een nieuwe OAuth toepassing op https://github.com/settings/applications/new
-auths.tip.gitlab=Registreer een nieuwe applicatie op https://gitlab.com/profile/applicaties
auths.tip.google_plus=Verkrijg OAuth2 client referenties van de Google API console op https://console.developers.google.com/
auths.tip.openid_connect=Gebruik de OpenID Connect Discovery URL (/.well-known/openid-configuration) om de eindpunten op te geven
auths.tip.yandex=`Maak een nieuwe applicatie aan op https://oauth.yandex.com/client/new. Selecteer de volgende machtigingen van de "Yandex". assport API sectie: "Toegang tot e-mailadres", "Toegang tot avatar" en "Toegang tot gebruikersnaam, voornaam en achternaam, geslacht"`
@@ -2537,6 +2520,7 @@ notices.desc=Beschrijving
notices.op=Op.
notices.delete_success=De systeemmeldingen zijn verwijderd.
+
[action]
create_repo=repository aangemaakt in %s
rename_repo=hernoemde repository van %[1]s
naar %[3]s
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index d713110a72..a1d7e95842 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -113,6 +113,14 @@ concept_user_organization=Organizacja
name=Nazwa
+filter.is_archived=Zarchiwizowane
+filter.is_template=Szablon
+filter.public=Publiczne
+filter.private=Prywatne
+
+
+[search]
+
[aria]
[heatmap]
@@ -251,7 +259,6 @@ collaborative_repos=Wspólne repozytoria
my_orgs=Moje organizacje
my_mirrors=Moje kopie lustrzane
view_home=Zobacz %s
-search_repos=Znajdź repozytorium…
filter=Inne filtry
filter_by_team_repositories=Filtruj według repozytoriów zespołu
feed_of=`Kanał "%s"`
@@ -272,14 +279,7 @@ issues.in_your_repos=W Twoich repozytoriach
repos=Repozytoria
users=Użytkownicy
organizations=Organizacje
-search=Szukaj
code=Kod
-search.fuzzy=Fuzzy
-search.match=Dopasuj
-repo_no_results=Nie znaleziono pasujących repozytoriów.
-user_no_results=Nie znaleziono pasującego użytkowników.
-org_no_results=Nie znaleziono pasujących organizacji.
-code_no_results=Nie znaleziono kodu źródłowego odpowiadającego Twojej frazie wyszukiwania.
code_last_indexed_at=Ostatnio indeksowane %s
[auth]
@@ -292,7 +292,6 @@ remember_me=Zapamiętaj to urządzenie
forgot_password_title=Zapomniałem hasła
forgot_password=Zapomniałeś hasła?
sign_up_now=Potrzebujesz konta? Zarejestruj się teraz.
-confirmation_mail_sent_prompt=Nowy email aktywacyjny został wysłany na adres %s. Sprawdź swoją skrzynkę odbiorczą w ciągu %s aby dokończyć proces rejestracji.
must_change_password=Zaktualizuj swoje hasło
allow_password_change=Użytkownik musi zmienić hasło (zalecane)
reset_password_mail_sent_prompt=E-mail potwierdzający został wysłany na adres %s. Sprawdź swoją skrzynkę odbiorczą w przeciągu %s, aby ukończyć proces odzyskiwania konta.
@@ -474,6 +473,7 @@ auth_failed=Uwierzytelnienie się nie powiodło: %v
target_branch_not_exist=Gałąź docelowa nie istnieje.
+
[user]
change_avatar=Zmień swój awatar…
repositories=Repozytoria
@@ -490,6 +490,7 @@ user_bio=Biografia
disabled_public_activity=Ten użytkownik wyłączył publiczne wyświetlanie jego aktywności.
+
[settings]
profile=Profil
account=Konto
@@ -596,7 +597,6 @@ gpg_invalid_token_signature=Podany klucz GPG, podpis i token nie pasują lub tok
gpg_token_required=Musisz podać podpis poniższego tokenu
gpg_token=Token
gpg_token_help=Możesz wygenerować podpis za pomocą:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Wzmocniony podpis GPG
key_signature_gpg_placeholder=Zaczyna się od '-----BEGIN PGP SIGNATURE-----'
ssh_key_verified=Zweryfikowany klucz
@@ -740,7 +740,6 @@ fork_repo=Forkuj repozytorium
fork_from=Forkuj z
fork_visibility_helper=Widoczność sforkowanego repozytorium nie może być zmieniona.
use_template=Użyj tego szablonu
-clone_in_vsc=Klonuj w VS Code
download_zip=Pobierz ZIP
download_tar=Pobierz TAR.GZ
download_bundle=Pobierz BUNDLE
@@ -973,8 +972,6 @@ editor.require_signed_commit=Gałąź wymaga podpisanych commitów
commits.desc=Przeglądaj historię zmian kodu źródłowego.
commits.commits=Commity
-commits.search=Przeszukaj commity…
-commits.find=Szukaj
commits.search_all=Wszystkie gałęzie
commits.author=Autor
commits.message=Wiadomość
@@ -1010,7 +1007,6 @@ projects.type.basic_kanban=Basic Kanban
projects.type.bug_triage=Bug Triage
projects.template.desc=Szablon projektu
projects.template.desc_helper=Wybierz szablon projektu do rozpoczęcia
-projects.type.uncategorized=Bez kategorii
projects.column.edit_title=Nazwa
projects.column.new_title=Nazwa
projects.column.color=Kolor
@@ -1279,7 +1275,6 @@ pulls.compare_changes_desc=Wybierz gałąź, do której chcesz scalić oraz gał
pulls.compare_base=scal do
pulls.compare_compare=ściągnij z
pulls.filter_branch=Filtruj branch
-pulls.no_results=Nie znaleziono wyników.
pulls.nothing_to_compare=Te gałęzie są sobie równe. Nie ma potrzeby tworzyć Pull Requesta.
pulls.nothing_to_compare_and_allow_empty_pr=Te gałęzie są równe. Ten PR będzie pusty.
pulls.create=Utwórz Pull Request
@@ -1467,12 +1462,7 @@ activity.git_stats_and_deletions=i
activity.git_stats_deletion_1=%d usunięcie
activity.git_stats_deletion_n=%d usunięć
-search=Szukaj
-search.search_repo=Przeszukaj repozytorium
-search.fuzzy=Fuzzy
-search.match=Dopasuj
-search.results=Wyniki wyszukiwania dla "%s" w %s
-search.code_no_results=Nie znaleziono kodu źródłowego odpowiadającego Twojej frazie wyszukiwania.
+contributors.contribution_type.commits=Commity
settings=Ustawienia
settings.desc=Ustawienia to miejsce, w którym możesz zmieniać parametry repozytorium
@@ -1583,7 +1573,6 @@ settings.delete_collaborator=Usuń
settings.collaborator_deletion=Usuń współpracownika
settings.collaborator_deletion_desc=Usunięcie współpracownika odbierze mu dostęp do tego repozytorium. Kontynuować?
settings.remove_collaborator_success=Usunięto użytkownika.
-settings.search_user_placeholder=Szukaj użytkownika…
settings.org_not_allowed_to_be_collaborator=Organizacji nie można dodać jako współpracownika.
settings.change_team_access_not_allowed=Zmiana dostępu zespołu do repozytorium zostało zastrzeżone do właściciela organizacji
settings.team_not_in_organization=Zespół nie jest w tej samej organizacji co repozytorium
@@ -1591,7 +1580,6 @@ settings.teams=Zespoły
settings.add_team=Dodaj zespół
settings.add_team_duplicate=Zespół już posiada repozytorium
settings.add_team_success=Zespół ma teraz dostęp do repozytorium.
-settings.search_team=Szukaj zespołu…
settings.change_team_permission_tip=Uprawnienia zespołu ustawione są konfigurowane na stronie ustawień zespołu i nie mogą być zmieniane dla pojedynczych repozytoriów
settings.delete_team_tip=Ten zespół ma dostęp do wszystkich repozytoriów i nie może zostać usunięty
settings.remove_team_success=Dostęp zespołu do repozytorium został usunięty.
@@ -1707,9 +1695,7 @@ settings.protect_whitelist_committers=Wypychanie ograniczone białą listą
settings.protect_whitelist_committers_desc=Tylko dopuszczeni użytkownicy oraz zespoły będą miały możliwość wypychania zmian do tej gałęzi (oprócz wymuszenia wypchnięcia).
settings.protect_whitelist_deploy_keys=Dozwolona lista kluczy wdrożeniowych z uprawnieniem zapisu do push'a.
settings.protect_whitelist_users=Użytkownicy dopuszczeni do wypychania:
-settings.protect_whitelist_search_users=Szukaj użytkowników…
settings.protect_whitelist_teams=Zespoły dopuszczone do wypychania:
-settings.protect_whitelist_search_teams=Szukaj zespołów…
settings.protect_merge_whitelist_committers=Włącz dopuszczenie scalania
settings.protect_merge_whitelist_committers_desc=Zezwól jedynie dopuszczonym użytkownikom lub zespołom na scalanie Pull Requestów w tej gałęzi.
settings.protect_merge_whitelist_users=Użytkownicy dopuszczeni do scalania:
@@ -1899,6 +1885,8 @@ error.csv.too_large=Nie można wyświetlić tego pliku, ponieważ jest on zbyt d
error.csv.unexpected=Nie można renderować tego pliku, ponieważ zawiera nieoczekiwany znak w wierszu %d i kolumnie %d.
error.csv.invalid_field_count=Nie można renderować tego pliku, ponieważ ma nieprawidłową liczbę pól w wierszu %d.
+[graphs]
+
[org]
org_name_holder=Nazwa organizacji
org_full_name_holder=Pełna nazwa organizacji
@@ -1989,7 +1977,6 @@ teams.write_permission_desc=Ten zespół udziela dostępu z zapisemadministratora: członkowie mogą wyświetlać i wypychać zmiany oraz dodawać współpracowników do repozytoriów zespołu.
teams.create_repo_permission_desc=Dodatkowo, ten zespół otrzyma uprawnienie Tworzenie repozytoriów: jego członkowie mogą tworzyć nowe repozytoria w organizacji.
teams.repositories=Repozytoria zespołu
-teams.search_repo_placeholder=Szukaj repozytorium…
teams.remove_all_repos_title=Usuń wszystkie repozytoria zespołu
teams.remove_all_repos_desc=Usunie to wszystkie repozytoria przypisane do zespołu.
teams.add_all_repos_title=Dodaj wszystkie repozytoria
@@ -2014,6 +2001,8 @@ hooks=Weebhook'i
authentication=Źródła uwierzytelniania
emails=Emaile użytkowników
config=Konfiguracja
+config_summary=Podsumowanie
+config_settings=Ustawienia
notices=Powiadomienia systemu
monitor=Monitorowanie
first_page=Pierwsza
@@ -2021,7 +2010,6 @@ last_page=Ostatnia
total=Ogółem: %d
dashboard.statistic=Podsumowanie
-dashboard.operations=Operacje konserwacji
dashboard.system_status=Status strony
dashboard.operation_name=Nazwa operacji
dashboard.operation_switch=Przełącz
@@ -2152,9 +2140,6 @@ repos.unadopted.no_more=Nie znaleziono więcej nieprzyjętych repozytoriów
repos.owner=Właściciel
repos.name=Nazwa
repos.private=Prywatne
-repos.watches=Obserwujących
-repos.stars=Polubienia
-repos.forks=Forki
repos.issues=Zgłoszenia
repos.size=Rozmiar
@@ -2243,7 +2228,6 @@ auths.tip.nextcloud=`Zarejestruj nowego klienta OAuth w swojej instancji za pomo
auths.tip.dropbox=Stwórz nową aplikację na https://www.dropbox.com/developers/apps
auths.tip.facebook=`Zarejestruj nową aplikację na https://developers.facebook.com/apps i dodaj produkt "Facebook Login"`
auths.tip.github=Zarejestruj nową aplikację OAuth na https://github.com/settings/applications/new
-auths.tip.gitlab=Zarejestruj nową aplikację na https://gitlab.com/profile/applications
auths.tip.google_plus=Uzyskaj dane uwierzytelniające klienta OAuth2 z konsoli Google API na https://console.developers.google.com/
auths.tip.openid_connect=Użyj adresu URL OpenID Connect Discovery (/.well-known/openid-configuration), aby określić punkty końcowe
auths.tip.twitter=Przejdź na https://dev.twitter.com/apps, stwórz aplikację i upewnij się, że opcja “Allow this application to be used to Sign in with Twitter” jest włączona
@@ -2425,6 +2409,7 @@ notices.desc=Opis
notices.op=Operacja
notices.delete_success=Powiadomienia systemu zostały usunięte.
+
[action]
create_repo=tworzy repozytorium %s
rename_repo=zmienia nazwę repozytorium %[1]s
na %[3]s
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index cf5fd0055c..45f1c3b3f8 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -90,6 +90,7 @@ remove=Remover
remove_all=Excluir todos
remove_label_str=`Remover item "%s"`
edit=Editar
+view=Visualizar
enabled=Habilitado
disabled=Desabilitado
@@ -97,6 +98,7 @@ locked=Bloqueado
copy=Copiar
copy_url=Copiar URL
+copy_hash=Copiar hash
copy_content=Copiar conteúdo
copy_branch=Copiar nome do branch
copy_success=Copiado!
@@ -109,6 +111,7 @@ loading=Carregando…
error=Erro
error404=A página que você está tentando acessar não existe ou você não está autorizado a visualizá-la.
+go_back=Voltar
never=Nunca
unknown=Desconhecido
@@ -119,6 +122,7 @@ pin=Fixar
unpin=Desfixar
artifacts=Artefatos
+confirm_delete_artifact=Tem certeza que deseja excluir o artefato '%s' ?
archived=Arquivado
@@ -137,6 +141,15 @@ confirm_delete_selected=Confirma a exclusão de todos os itens selecionados?
name=Nome
value=Valor
+filter=Filtro
+filter.is_archived=Arquivado
+filter.is_template=Template
+filter.public=Pública
+filter.private=Privado
+
+
+[search]
+
[aria]
navbar=Barra de navegação
footer=Rodapé
@@ -309,7 +322,6 @@ collaborative_repos=Repositórios colaborativos
my_orgs=Minhas organizações
my_mirrors=Meus espelhos
view_home=Ver %s
-search_repos=Encontre um repositório…
filter=Outros filtros
filter_by_team_repositories=Filtrar por repositórios da equipe
feed_of=`Feed de "%s"`
@@ -330,20 +342,8 @@ issues.in_your_repos=Em seus repositórios
repos=Repositórios
users=Usuários
organizations=Organizações
-search=Pesquisar
go_to=Ir para
code=Código
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Similar
-search.fuzzy.tooltip=Incluir resultados que sejam próximos ao termo de busca
-search.match=Correspondência
-search.match.tooltip=Incluir somente resultados que correspondam exatamente ao termo de busca
-code_search_unavailable=A pesquisa por código não está disponível no momento. Entre em contato com o administrador do site.
-repo_no_results=Nenhum repositório correspondente foi encontrado.
-user_no_results=Nenhum usuário correspondente foi encontrado.
-org_no_results=Nenhuma organização correspondente foi encontrada.
-code_no_results=Nenhum código-fonte correspondente ao seu termo de pesquisa foi encontrado.
-code_search_results=`Resultados da pesquisa por: "%s"`
code_last_indexed_at=Última indexação %s
relevant_repositories_tooltip=Repositórios que são forks ou que não possuem tópico, nem ícone e nem descrição estão ocultos.
relevant_repositories=Apenas repositórios relevantes estão sendo mostrados, mostrar resultados não filtrados.
@@ -356,11 +356,11 @@ disable_register_prompt=Cadastro está desabilitado. Entre em contato com o admi
disable_register_mail=E-mail de confirmação de cadastro está desabilitado.
manual_activation_only=Entre em contato com o administrador do site para concluir a ativação.
remember_me=Lembrar deste Dispositivo
+remember_me.compromised=O token de login não é mais válido, o que pode indicar uma conta comprometida. Por favor, verifique a sua conta por atividades incomuns.
forgot_password_title=Esqueci minha senha
forgot_password=Esqueceu sua senha?
sign_up_now=Precisa de uma conta? Cadastre-se agora.
sign_up_successful=A conta foi criada com sucesso. Bem-vindo!
-confirmation_mail_sent_prompt=Um novo e-mail de confirmação foi enviado para %s. Por favor, verifique sua caixa de e-mail nas próximas %s horas para finalizar o processo de cadastro.
must_change_password=Redefina sua senha
allow_password_change=Exigir que o usuário redefina a senha (recomendado)
reset_password_mail_sent_prompt=Um e-mail de confirmação foi enviado para %s. Por favor, verifique sua caixa de entrada dentro do(s) próximo(s) %s para concluir o processo de recuperação de conta.
@@ -417,6 +417,7 @@ authorization_failed_desc=A autorização falhou porque detectamos uma solicita
sspi_auth_failed=Falha de autenticação SSPI
password_pwned=A senha que você escolheu faz parte de uma lista de senhas roubadas expostas anteriormente em violações de dados. Tente novamente com uma senha diferente e considere alterar essa senha em outro lugar também.
password_pwned_err=Não foi possível concluir a requisição ao HaveIBeenPwned
+last_admin=Você não pode remover o último administrador. Deve haver pelo menos um administrador.
[mail]
view_it_on=Veja em %s
@@ -582,6 +583,8 @@ org_still_own_packages=Esta organização ainda possui pacotes, exclua-os primei
target_branch_not_exist=O branch de destino não existe.
+admin_cannot_delete_self=Você não pode excluir você mesmo quando você é um administrador. Por favor, remova seus privilégios de administrador primeiro.
+
[user]
change_avatar=Altere seu avatar...
joined_on=Inscreveu-se em %s
@@ -607,6 +610,7 @@ form.name_reserved=O nome de usuário "%s" está reservado.
form.name_pattern_not_allowed=O padrão de "%s" não é permitido em um nome de usuário.
form.name_chars_not_allowed=Nome de usuário "%s" contém caracteres inválidos.
+
[settings]
profile=Perfil
account=Conta
@@ -751,7 +755,6 @@ gpg_invalid_token_signature=A chave GPG fornecida, a assinatura ou o token não
gpg_token_required=Você tem que fornecer uma assinatura para o token abaixo
gpg_token=Token
gpg_token_help=Você pode gerar uma assinatura usando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Assinatura GPG blindada
key_signature_gpg_placeholder=Começa com '-----BEGIN PGP SIGNATURE-----'
verify_gpg_key_success=A chave GPG "%s" foi validada.
@@ -858,6 +861,7 @@ revoke_oauth2_grant_description=Revogando o acesso para este aplicativo de terce
revoke_oauth2_grant_success=Acesso revogado com sucesso.
twofa_desc=Autenticação de dois fatores melhora a segurança de sua conta.
+twofa_recovery_tip=Se você perder o seu dispositivo, você será capaz de usar uma chave de recuperação de uso único para recuperar o acesso à sua conta.
twofa_is_enrolled=Sua conta está atualmente habilitada com autenticação de dois fatores.
twofa_not_enrolled=Sua conta não está atualmente inscrita para a autenticação em duas etapas.
twofa_disable=Desabilitar a autenticação de dois fatores
@@ -880,6 +884,8 @@ webauthn_register_key=Adicionar chave de segurança
webauthn_nickname=Apelido
webauthn_delete_key=Remover chave de segurança
webauthn_delete_key_desc=Se você remover uma chave de segurança, não poderá mais entrar com ela. Continuar?
+webauthn_key_loss_warning=Se você perder suas chaves de segurança, perderá o acesso à sua conta.
+webauthn_alternative_tip=Você pode querer configurar um método de autenticação adicional.
manage_account_links=Gerenciar contas vinculadas
manage_account_links_desc=Estas contas externas estão vinculadas a sua conta de Gitea.
@@ -916,6 +922,7 @@ visibility.private=Privada
visibility.private_tooltip=Visível apenas para membros das organizações às quais você se associou
[repo]
+new_repo_helper=Um repositório contém todos os arquivos do projeto, inclusive o histórico de revisões. Já está hospedando um em outro lugar? Migre o repositório.
owner=Proprietário
owner_helper=Algumas organizações podem não aparecer no menu devido a um limite de contagem dos repositórios.
repo_name=Nome do repositório
@@ -936,9 +943,10 @@ fork_from=Fork de
already_forked=Você já fez o fork de %s
fork_to_different_account=Faça um fork para uma conta diferente
fork_visibility_helper=A visibilidade do fork de um repositório não pode ser alterada.
+fork_branch=Branch a ser clonado para o fork
+all_branches=Todos os branches
fork_no_valid_owners=Não é possível fazer um fork desse repositório porque não há proprietários validos.
use_template=Usar este modelo
-clone_in_vsc=Clonar no VS Code
download_zip=Baixar ZIP
download_tar=Baixar TAR.GZ
download_bundle=Baixar PACOTE
@@ -971,6 +979,7 @@ mirror_prune=Varrer
mirror_prune_desc=Remover referências obsoletas de controle remoto
mirror_interval=Intervalo de espelhamento (unidades válidas são 'h', 'm', ou 's'). O desabilita a sincronização automática. (Intervalo mínimo: %s)
mirror_interval_invalid=O intervalo do espelhamento não é válido.
+mirror_sync=sincronizado
mirror_sync_on_commit=Sincronizar quando commits forem enviados
mirror_address=Clonar de URL
mirror_address_desc=Coloque todas as credenciais necessárias na seção de autorização.
@@ -1016,6 +1025,7 @@ desc.public=Público
desc.template=Template
desc.internal=Interno
desc.archived=Arquivado
+desc.sha256=SHA256
template.items=Itens do modelo
template.git_content=Conteúdo Git (Branch padrão)
@@ -1166,6 +1176,7 @@ audio_not_supported_in_browser=Seu navegador não suporta a tag 'audio' do HTML5
stored_lfs=Armazenado com Git LFS
symbolic_link=Link simbólico
executable_file=Arquivo executável
+generated=Gerado
commit_graph=Gráfico de commits
commit_graph.select=Selecionar branches
commit_graph.hide_pr_refs=Esconder Pull Requests
@@ -1252,9 +1263,7 @@ commits.desc=Veja o histórico de alterações do código de fonte.
commits.commits=Commits
commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes.
commits.nothing_to_compare=Estes branches são iguais.
-commits.search=Pesquisar commits...
commits.search.tooltip=Você pode prefixar as palavras-chave com "author:" (autor da mudança), "committer:" (autor do commit), "after:" (depois) ou "before:" (antes). Por exemplo: "revert author:Ana before:2019-01-13".\
-commits.find=Pesquisar
commits.search_all=Todos os branches
commits.author=Autor
commits.message=Mensagem
@@ -1266,6 +1275,7 @@ commits.signed_by_untrusted_user=Assinado por usuário não confiável
commits.signed_by_untrusted_user_unmatched=Assinado por usuário não confiável que não corresponde ao autor da submissão
commits.gpg_key_id=ID da chave GPG
commits.ssh_key_fingerprint=Impressão Digital da Chave SSH
+commits.view_path=Visualizar neste ponto do histórico
commit.operations=Operações
commit.revert=Reverter
@@ -1304,7 +1314,6 @@ projects.type.basic_kanban=Kanban básico
projects.type.bug_triage=Triagem de Bugs
projects.template.desc=Modelo de projeto
projects.template.desc_helper=Selecione um modelo de projeto para começar
-projects.type.uncategorized=Sem categoria
projects.column.edit=Editar coluna
projects.column.edit_title=Nome
projects.column.new_title=Nome
@@ -1312,10 +1321,7 @@ projects.column.new_submit=Criar coluna
projects.column.new=Nova Coluna
projects.column.set_default=Atribuir como padrão
projects.column.set_default_desc=Definir esta coluna como padrão para pull e issues sem categoria
-projects.column.unset_default=Desatribuir padrão
-projects.column.unset_default_desc=Desatribuir esta coluna como padrão
projects.column.delete=Excluir coluna
-projects.column.deletion_desc=Excluir uma coluna do projeto move todas as issues relacionadas para 'Sem categoria'. Continuar?
projects.column.color=Cor
projects.open=Abrir
projects.close=Fechar
@@ -1356,6 +1362,7 @@ issues.choose.blank=Padrão
issues.choose.blank_about=Criar uma issue a partir do modelo padrão.
issues.choose.ignore_invalid_templates=Modelos inválidos foram ignorados
issues.choose.invalid_templates=%v modelo(s) inválido(s) encontrado(s)
+issues.choose.invalid_config=A configuração da issue contém erros:
issues.no_ref=Nenhum branch/tag especificado
issues.create=Criar issue
issues.new_label=Nova etiqueta
@@ -1426,7 +1433,6 @@ issues.filter_sort.moststars=Mais estrelas
issues.filter_sort.feweststars=Menos estrelas
issues.filter_sort.mostforks=Mais forks
issues.filter_sort.fewestforks=Menos forks
-issues.keyword_search_unavailable=A pesquisa por palavra-chave não está disponível no momento. Entre em contato com o administrador do site.
issues.action_open=Abrir
issues.action_close=Fechar
issues.action_label=Etiqueta
@@ -1479,6 +1485,11 @@ issues.author_helper=Este usuário é o autor.
issues.role.owner=Proprietário
issues.role.owner_helper=Este usuário é o dono deste repositório.
issues.role.member=Membro
+issues.role.collaborator=Colaborador
+issues.role.collaborator_helper=Este usuário foi convidado para colaborar no repositório.
+issues.role.first_time_contributor=Primeira vez contribuindo
+issues.role.first_time_contributor_helper=Esta é a primeira contribuição deste usuário para o repositório.
+issues.role.contributor=Contribuidor
issues.re_request_review=Re-solicitar revisão
issues.is_stale=Houve alterações nessa PR desde essa revisão
issues.remove_request_review=Remover solicitação de revisão
@@ -1494,6 +1505,8 @@ issues.label_description=Descrição da etiqueta
issues.label_color=Cor da etiqueta
issues.label_exclusive=Exclusivo
issues.label_archive=Arquivar etiqueta
+issues.label_archived_filter=Mostrar etiquetas arquivadas
+issues.label_archive_tooltip=Etiquetas arquivadas são excluídas, por padrão, das sugestões ao pesquisar por etiqueta.
issues.label_exclusive_desc=Nomeie o rótulo escopo/item
para torná-lo mutuamente exclusivo com outros rótulos do escopo/
.
issues.label_exclusive_warning=Quaisquer rótulos com escopo conflitantes serão removidos ao editar os rótulos de uma issue ou pull request.
issues.label_count=%d etiquetas
@@ -1572,6 +1585,7 @@ issues.due_date_form=dd/mm/aaaa
issues.due_date_form_add=Adicionar data limite
issues.due_date_form_edit=Editar
issues.due_date_form_remove=Remover
+issues.due_date_not_writer=Você precisa de acesso de gravação a esse repositório para atualizar a data limite de uma issue.
issues.due_date_not_set=Data limite não informada.
issues.due_date_added=adicionou a data limite %s %s
issues.due_date_modified=modificou a data limite de %[2]s para %[1]s %[3]s
@@ -1668,7 +1682,6 @@ pulls.compare_compare=pull de
pulls.switch_comparison_type=Mudar tipo de comparação
pulls.switch_head_and_base=Trocar cabeça e base
pulls.filter_branch=Filtrar branch
-pulls.no_results=Nada encontrado.
pulls.show_all_commits=Mostrar todos os commits
pulls.show_changes_since_your_last_review=Mostrar alterações desde sua última revisão
pulls.showing_only_single_commit=Mostrando apenas as alterações do commit %[1]s
@@ -1735,6 +1748,7 @@ pulls.merge_pull_request=Criar commit de merge
pulls.rebase_merge_pull_request=Rebase e fast-forward
pulls.rebase_merge_commit_pull_request=Rebase e criar commit de merge
pulls.squash_merge_pull_request=Criar commit de squash
+pulls.fast_forward_only_merge_pull_request=Apenas Fast-forward
pulls.merge_manually=Merge feito manualmente
pulls.merge_commit_id=A ID de merge commit
pulls.require_signed_wont_sign=O branch requer commits assinados, mas este merge não será assinado
@@ -1758,6 +1772,8 @@ pulls.status_checks_failure=Algumas verificações falharam
pulls.status_checks_error=Algumas verificações reportaram erros
pulls.status_checks_requested=Obrigatário
pulls.status_checks_details=Detalhes
+pulls.status_checks_hide_all=Ocultar todas as verificações
+pulls.status_checks_show_all=Mostrar todas as verificações
pulls.update_branch=Atualizar branch por merge
pulls.update_branch_rebase=Atualizar branch por rebase
pulls.update_branch_success=Atualização do branch foi bem-sucedida
@@ -1766,6 +1782,9 @@ pulls.outdated_with_base_branch=Este branch está desatualizado com o branch bas
pulls.close=Fechar pull request
pulls.closed_at=`fechou este pull request %[2]s`
pulls.reopened_at=`reabriu este pull request %[2]s`
+pulls.cmd_instruction_checkout_title=Checkout
+pulls.cmd_instruction_merge_title=Merge
+pulls.cmd_instruction_merge_desc=Faça merge das alterações e atualize no Gitea.
pulls.clear_merge_message=Limpar mensagem do merge
pulls.clear_merge_message_hint=Limpar a mensagem de merge só irá remover o conteúdo da mensagem de commit e manter trailers git gerados, como "Co-Authored-By …".
@@ -1862,6 +1881,8 @@ wiki.page_name_desc=Digite um nome para esta página Wiki. Alguns nomes especiai
wiki.original_git_entry_tooltip=Ver o arquivo Git original em vez de usar o link amigável.
activity=Atividade
+activity.navbar.pulse=Pulso
+activity.navbar.contributors=Contribuidores
activity.period.filter_label=Período:
activity.period.daily=1 dia
activity.period.halfweekly=3 dias
@@ -1927,16 +1948,10 @@ activity.git_stats_and_deletions=e
activity.git_stats_deletion_1=%d exclusão
activity.git_stats_deletion_n=%d exclusões
-search=Pesquisar
-search.search_repo=Pesquisar no repositório...
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir resultados que sejam próximos ao termo de busca
-search.match=Corresponde
-search.match.tooltip=Incluir somente resultados que correspondam exatamente ao termo de busca
-search.results=Resultados da pesquisa para "%s" em %s
-search.code_no_results=Nenhum código-fonte correspondente ao seu termo de pesquisa foi encontrado.
-search.code_search_unavailable=A pesquisa por código não está disponível no momento. Entre em contato com o administrador do site.
+contributors.contribution_type.filter_label=Tipo de contribuição:
+contributors.contribution_type.commits=Commits
+contributors.contribution_type.additions=Adições
+contributors.contribution_type.deletions=Exclusões
settings=Configurações
settings.desc=Opções é onde você pode gerenciar as configurações para o repositório
@@ -2005,6 +2020,7 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir edições de manten
settings.releases_desc=Habilitar versões do Repositório
settings.packages_desc=Habilitar Registro de Pacotes de Repositório
settings.projects_desc=Habilitar Projetos do Repositório
+settings.projects_mode_all=Todos os projetos
settings.actions_desc=Habilitar ações do repositório
settings.admin_settings=Configurações do administrador
settings.admin_enable_health_check=Habilitar verificações de integridade (git fsck) no repositório
@@ -2076,7 +2092,6 @@ settings.delete_collaborator=Remover
settings.collaborator_deletion=Remover colaborador
settings.collaborator_deletion_desc=A exclusão de um colaborador irá revogar o acesso a este repositório. Continuar?
settings.remove_collaborator_success=O colaborador foi removido.
-settings.search_user_placeholder=Pesquisar usuário...
settings.org_not_allowed_to_be_collaborator=Organizações não podem ser adicionadas como um colaborador.
settings.change_team_access_not_allowed=Alteração do acesso da equipe para o repositório está restrito ao proprietário da organização
settings.team_not_in_organization=A equipe não está na mesma organização que o repositório
@@ -2084,7 +2099,6 @@ settings.teams=Equipes
settings.add_team=Adicionar Equipe
settings.add_team_duplicate=A equipe já tem o repositório
settings.add_team_success=A equipe agora tem acesso ao repositório.
-settings.search_team=Pesquisar Equipe…
settings.change_team_permission_tip=A permissão da equipe está definida na página de configurações da equipe e não pode ser alterada por repositório
settings.delete_team_tip=Esta equipe tem acesso a todos os repositórios e não pode ser removida
settings.remove_team_success=O acesso da equipe ao repositório foi removido.
@@ -2229,9 +2243,7 @@ settings.protect_whitelist_committers=Lista permitida para push
settings.protect_whitelist_committers_desc=Somente usuários ou equipes da lista permitida serão autorizados realizar push neste branch (mas não forçar o push).
settings.protect_whitelist_deploy_keys=Dar permissão às chaves de deploy com acesso de gravação para push.
settings.protect_whitelist_users=Usuários com permissão para realizar push:
-settings.protect_whitelist_search_users=Pesquisar usuários...
settings.protect_whitelist_teams=Equipes com permissão para realizar push:
-settings.protect_whitelist_search_teams=Pesquisar equipes...
settings.protect_merge_whitelist_committers=Habilitar controle de permissão de merge
settings.protect_merge_whitelist_committers_desc=Permitir que determinados usuários ou equipes possam aplicar merge de pull requests neste branch.
settings.protect_merge_whitelist_users=Usuários com permissão para aplicar merge:
@@ -2298,6 +2310,9 @@ settings.archive.error=Um erro ocorreu enquanto estava sendo arquivado o reposit
settings.archive.error_ismirror=Você não pode arquivar um repositório espelhado.
settings.archive.branchsettings_unavailable=Configurações do branch não estão disponíveis quando o repositório está arquivado.
settings.archive.tagsettings_unavailable=As configurações de tag não estão disponíveis se o repositório estiver arquivado.
+settings.unarchive.button=Desarquivar o repositório
+settings.unarchive.header=Desarquivar este repositório
+settings.unarchive.success=O repositório foi desarquivado com sucesso.
settings.update_avatar_success=O avatar do repositório foi atualizado.
settings.lfs=LFS
settings.lfs_filelist=Arquivos LFS armazenados neste repositório
@@ -2420,6 +2435,7 @@ release.edit_release=Atualizar versão
release.delete_release=Excluir versão
release.delete_tag=Apagar Tag
release.deletion=Excluir versão
+release.deletion_desc=A exclusão de uma versão apenas a remove do Gitea. Isso não afetará a tag do Git, o conteúdo do seu repositório ou seu histórico. Continuar?
release.deletion_success=A versão foi excluída.
release.deletion_tag_desc=A tag será excluída do repositório. Conteúdo do repositório e histórico permanecerão inalterados. Continuar?
release.deletion_tag_success=A tag foi excluída.
@@ -2484,6 +2500,11 @@ error.csv.too_large=Não é possível renderizar este arquivo porque ele é muit
error.csv.unexpected=Não é possível renderizar este arquivo porque ele contém um caractere inesperado na linha %d e coluna %d.
error.csv.invalid_field_count=Não é possível renderizar este arquivo porque ele tem um número errado de campos na linha %d.
+[graphs]
+component_loading=Carregando %s...
+component_loading_failed=Não foi possível carregar %s
+component_loading_info=Isto pode demorar um pouco…
+
[org]
org_name_holder=Nome da organização
org_full_name_holder=Nome completo da organização
@@ -2513,6 +2534,7 @@ form.create_org_not_allowed=Você não tem permissão para criar uma organizaç
settings=Configurações
settings.options=Organização
settings.full_name=Nome completo
+settings.email=E-mail de contato
settings.website=Site
settings.location=Localização
settings.permission=Permissões
@@ -2585,7 +2607,6 @@ teams.write_permission_desc=Esta equipe concede acesso para escritaAdministrador: Membros podem ler, fazer push e adicionar outros colaboradores para os repositórios da equipe.
teams.create_repo_permission_desc=Além disso, esta equipe concede permissão de Criar repositório: membros podem criar novos repositórios na organização.
teams.repositories=Repositórios da equipe
-teams.search_repo_placeholder=Pesquisar repositório...
teams.remove_all_repos_title=Remover todos os repositórios da equipe
teams.remove_all_repos_desc=Isto irá remover todos os repositórios da equipe.
teams.add_all_repos_title=Adicionar todos os repositórios
@@ -2601,11 +2622,13 @@ teams.all_repositories_helper=A equipe tem acesso a todos os repositórios. Sele
teams.all_repositories_read_permission_desc=Esta equipe concede acesso Leitura a todos os repositórios: membros podem ver e clonar repositórios.
teams.all_repositories_write_permission_desc=Esta equipe concede acesso Escrita a todos os repositórios: os membros podem ler de e fazer push para os repositórios.
teams.all_repositories_admin_permission_desc=Esta equipe concede acesso Administrativo a todos os repositórios: os membros podem ler, fazer push e adicionar colaboradores aos repositórios.
+teams.invite.title=Você foi convidado para fazer parte da equipe %s na organização %s.
teams.invite.by=Convidado por %s
teams.invite.description=Por favor, clique no botão abaixo para se juntar à equipe.
[admin]
dashboard=Painel
+identity_access=Identidade e acesso
users=Contas de usuário
organizations=Organizações
repositories=Repositórios
@@ -2614,15 +2637,17 @@ integrations=Integrações
authentication=Fontes de autenticação
emails=E-mails do Usuário
config=Configuração
+config_summary=Resumo
+config_settings=Configurações
notices=Avisos do sistema
monitor=Monitoramento
first_page=Primeira
last_page=Última
total=Total: %d
+settings=Configurações de Administrador
dashboard.new_version_hint=Uma nova versão está disponível: %s. Versão atual: %s. Visite o blog para mais informações.
dashboard.statistic=Resumo
-dashboard.operations=Operações de manutenção
dashboard.system_status=Status do sistema
dashboard.operation_name=Nome da operação
dashboard.operation_switch=Trocar
@@ -2777,9 +2802,6 @@ repos.unadopted.no_more=Não foram encontrados mais repositórios não adotados
repos.owner=Proprietário
repos.name=Nome
repos.private=Privado
-repos.watches=Observadores
-repos.stars=Favoritos
-repos.forks=Forks
repos.issues=Issues
repos.size=Tamanho
repos.lfs_size=Tamanho do LFS
@@ -2901,7 +2923,6 @@ auths.tip.nextcloud=`Registre um novo consumidor OAuth em sua instância usando
auths.tip.dropbox=Criar um novo aplicativo em https://www.dropbox.com/developers/apps
auths.tip.facebook=`Cadastrar um novo aplicativo em https://developers.facebook.com/apps e adicionar o produto "Facebook Login"`
auths.tip.github=Cadastrar um novo aplicativo de OAuth na https://github.com/settings/applications/new
-auths.tip.gitlab=Cadastrar um novo aplicativo em https://gitlab.com/profile/applications
auths.tip.google_plus=Obter credenciais de cliente OAuth2 do console de API do Google em https://console.developers.google.com/
auths.tip.openid_connect=Use o OpenID Connect Discovery URL (/.well-known/openid-configuration) para especificar os endpoints
auths.tip.twitter=Vá em https://dev.twitter.com/apps, crie um aplicativo e certifique-se de que está habilitada a opção “Allow this application to be used to Sign in with Twitter“
@@ -3110,6 +3131,7 @@ notices.desc=Descrição
notices.op=Op.
notices.delete_success=Os avisos do sistema foram excluídos.
+
[action]
create_repo=criou o repositório %s
rename_repo=renomeou o repositório %[1]s
para %[3]s
@@ -3293,6 +3315,8 @@ rpm.registry=Configure este registro pela linha de comando:
rpm.distros.redhat=em distribuições baseadas no RedHat
rpm.distros.suse=em distribuições baseadas no SUSE
rpm.install=Para instalar o pacote, execute o seguinte comando:
+rpm.repository=Informações do repositório
+rpm.repository.architectures=Arquiteturas
rubygems.install=Para instalar o pacote usando gem, execute o seguinte comando:
rubygems.install2=ou adicione-o ao Gemfile:
rubygems.dependencies.runtime=Dependências de Execução
@@ -3406,13 +3430,20 @@ runners.status.idle=Inativo
runners.status.active=Ativo
runners.status.offline=Offiline
runners.version=Versão
+runners.reset_registration_token=Redefinir token de registro
runners.reset_registration_token_success=Token de registro de runner redefinido com sucesso
runs.all_workflows=Todos os Workflows
runs.commit=Commit
+runs.scheduled=Agendado
runs.pushed_by=push feito por
runs.invalid_workflow_helper=O arquivo de configuração do workflow é inválido. Por favor, verifique seu arquivo de configuração: %s
+runs.actor=Ator
runs.status=Status
+runs.actors_no_select=Todos os atores
+runs.status_no_select=Todos os Status
+runs.no_results=Não houve correspondência de resultados.
+runs.empty_commit_message=(mensagem de commit vazia)
need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork.
@@ -3425,5 +3456,9 @@ type-3.display_name=Projeto da organização
[git.filemode]
; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", …
+directory=Diretório
+normal_file=Arquivo normal
+executable_file=Arquivo executável
symbolic_link=Link simbólico
+submodule=Submódulo
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 863a1545c3..59b3d3df67 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -25,6 +25,7 @@ enable_javascript=Este sítio Web requer JavaScript.
toc=Índice
licenses=Licenças
return_to_gitea=Retornar ao Gitea
+more_items=Mais itens
username=Nome de utilizador
email=Endereço de email
@@ -113,6 +114,7 @@ loading=Carregando…
error=Erro
error404=A página que pretende aceder não existe ou não tem autorização para a ver.
go_back=Voltar
+invalid_data=Dados inválidos: %v
never=Nunca
unknown=Desconhecido
@@ -123,6 +125,7 @@ pin=Fixar
unpin=Desafixar
artifacts=Artefactos
+confirm_delete_artifact=Tem a certeza que quer eliminar este artefacto "%s"?
archived=Arquivado
@@ -141,6 +144,43 @@ confirm_delete_selected=Confirma a exclusão de todos os itens marcados?
name=Nome
value=Valor
+filter=Filtro
+filter.clear=Retirar filtro
+filter.is_archived=Arquivado
+filter.not_archived=Não arquivado
+filter.is_fork=Derivado
+filter.not_fork=Não derivado
+filter.is_mirror=Replicado
+filter.not_mirror=Não replicado
+filter.is_template=Modelo
+filter.not_template=Não é modelo
+filter.public=Público
+filter.private=Privado
+
+no_results_found=Não foram encontrados quaisquer resultados.
+
+[search]
+search=Pesquisar...
+type_tooltip=Tipo de pesquisa
+fuzzy=Aproximada
+fuzzy_tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
+match=Fiel
+match_tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
+repo_kind=Pesquisar repositórios...
+user_kind=Pesquisar utilizadores...
+org_kind=Pesquisar organizações...
+team_kind=Pesquisar equipas...
+code_kind=Pesquisar código...
+code_search_unavailable=A pesquisa de código não está disponível, neste momento. Entre em contacto com o administrador.
+code_search_by_git_grep=Os resultados da pesquisa no código-fonte neste momento são fornecidos pelo "git grep". Esses resultados podem ser melhores se o administrador habilitar o indexador do repositório.
+package_kind=Pesquisar pacotes...
+project_kind=Pesquisar planeamentos...
+branch_kind=Pesquisar ramos...
+commit_kind=Pesquisar cometimentos...
+runner_kind=Pesquisar executores...
+no_results=Não foram encontrados resultados correspondentes.
+keyword_search_unavailable=Pesquisar por palavra-chave não está disponível, neste momento. Entre em contacto com o administrador.
+
[aria]
navbar=Barra de navegação
footer=Rodapé
@@ -246,6 +286,7 @@ email_title=Configurações de email
smtp_addr=Servidor SMTP
smtp_port=Porto do SMTP
smtp_from=Email do remetente
+smtp_from_invalid=O endereço para "Enviar email como" é inválido
smtp_from_helper=Endereço de email que o Gitea vai usar. Insira um endereço de email simples ou use o formato "Nome" .
mailer_user=Nome de utilizador do SMTP
mailer_password=Senha do SMTP
@@ -305,6 +346,7 @@ env_config_keys=Configuração do ambiente
env_config_keys_prompt=As seguintes variáveis de ambiente também serão aplicadas ao seu ficheiro de configuração:
[home]
+nav_menu=Menu de navegação
uname_holder=Nome de utilizador ou endereço de email
password_holder=Senha
switch_dashboard_context=Trocar contexto do painel
@@ -314,7 +356,6 @@ collaborative_repos=Repositórios colaborativos
my_orgs=As minhas organizações
my_mirrors=As minhas réplicas
view_home=Ver %s
-search_repos=Procurar um repositório…
filter=Outros filtros
filter_by_team_repositories=Filtrar por repositórios da equipa
feed_of=`Fonte de "%s"`
@@ -335,20 +376,8 @@ issues.in_your_repos=Nos seus repositórios
repos=Repositórios
users=Utilizadores
organizations=Organizações
-search=Procurar
go_to=Ir para
code=Código
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
-search.match=Fiel
-search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
-code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador.
-repo_no_results=Não foram encontrados quaisquer repositórios correspondentes.
-user_no_results=Não foram encontrados quaisquer utilizadores correspondentes.
-org_no_results=Não foram encontradas quaisquer organizações correspondentes.
-code_no_results=Não foi encontrado qualquer código-fonte correspondente à sua pesquisa.
-code_search_results=`Resultados da pesquisa para "%s"`
code_last_indexed_at=Última indexação %s
relevant_repositories_tooltip=Repositórios que são derivações ou que não têm tópico, nem ícone, nem descrição, estão escondidos.
relevant_repositories=Apenas estão a ser mostrados os repositórios relevantes. Mostrar resultados não filtrados.
@@ -366,7 +395,7 @@ forgot_password_title=Esqueci-me da senha
forgot_password=Esqueceu a sua senha?
sign_up_now=Precisa de uma conta? Inscreva-se agora.
sign_up_successful=A conta foi criada com sucesso. Bem-vindo/a!
-confirmation_mail_sent_prompt=Foi enviado um novo email de confirmação para %s. Verifique a sua caixa de entrada dentro de %s para completar o processo de inscrição.
+confirmation_mail_sent_prompt_ex=Foi enviado um email de confirmação para %s. Verifique a sua caixa de entrada dentro de %s para completar o processo de registo. Se o seu endereço de email de registo estiver errado, pode iniciar a sessão novamente e mudá-lo.
must_change_password=Mude a sua senha
allow_password_change=Exigir que o utilizador mude a senha (recomendado)
reset_password_mail_sent_prompt=Foi enviado um email de confirmação para %s. Verifique a sua caixa de entrada dentro de %s para completar o processo de recuperação.
@@ -376,6 +405,7 @@ prohibit_login=Início de sessão proibido
prohibit_login_desc=A sua conta está proibida de iniciar sessão. Contacte o administrador.
resent_limit_prompt=Já fez um pedido recentemente para enviar um email para pôr a conta em funcionamento. Espere 3 minutos e tente novamente.
has_unconfirmed_mail=Olá %s, tem um endereço de email não confirmado (%s). Se não recebeu um email de confirmação ou precisa de o voltar a enviar, clique no botão abaixo.
+change_unconfirmed_mail_address=Se o seu endereço de email estiver errado, pode mudá-lo aqui e enviar um novo email de confirmação.
resend_mail=Clique aqui para voltar a enviar um email para pôr a conta em funcionamento
email_not_associate=O endereço de email não está associado a qualquer conta.
send_reset_mail=Enviar email de recuperação da conta
@@ -423,6 +453,7 @@ authorization_failed_desc=A autorização falhou porque encontrámos um pedido i
sspi_auth_failed=Falhou a autenticação SSPI
password_pwned=A senha utilizada está numa lista de senhas roubadas anteriormente expostas em fugas de dados públicas. Tente novamente com uma senha diferente e considere também mudar esta senha nos outros sítios.
password_pwned_err=Não foi possível completar o pedido ao HaveIBeenPwned
+last_admin=Não pode remover o último administrador. Tem que existir pelo menos um administrador.
[mail]
view_it_on=Ver em %s
@@ -555,6 +586,7 @@ team_name_been_taken=O nome da equipa já foi tomado.
team_no_units_error=Permitir acesso a pelo menos uma secção do repositório.
email_been_used=O endereço de email já está em uso.
email_invalid=O endereço de email é inválido.
+email_domain_is_not_allowed=O domínio do email de utilizador %s entra en conflito com o EMAIL_DOMAIN_ALLOWLIST ou com o EMAIL_DOMAIN_BLOCKLIST. Verifique se a operação estava prevista.
openid_been_used=O endereço OpenID "%s" já está em uso.
username_password_incorrect=O nome de utilizador ou a senha estão errados.
password_complexity=A senha não passa nos requisitos de complexidade:
@@ -566,6 +598,8 @@ enterred_invalid_repo_name=O nome do repositório que inseriu está errado.
enterred_invalid_org_name=O nome da organização que inseriu está errado.
enterred_invalid_owner_name=O novo nome de proprietário não é válido.
enterred_invalid_password=A senha que inseriu está errada.
+unset_password=O utilizador não definiu a senha.
+unsupported_login_type=O tipo de início de sessão não é suportado para eliminar a conta.
user_not_exist=O utilizador não existe.
team_not_exist=A equipa não existe.
last_org_owner=Não pode remover o último utilizador da equipa 'proprietários'. Tem que haver pelo menos um proprietário numa organização.
@@ -588,6 +622,8 @@ org_still_own_packages=Esta organização ainda possui um ou mais pacotes, elimi
target_branch_not_exist=O ramo de destino não existe.
+admin_cannot_delete_self=Não se pode auto-remover quando tem privilégios de administração. Remova esses privilégios primeiro.
+
[user]
change_avatar=Mude o seu avatar…
joined_on=Inscreveu-se em %s
@@ -613,6 +649,30 @@ form.name_reserved=O nome de utilizador "%s" está reservado.
form.name_pattern_not_allowed=O padrão "%s" não é permitido no nome de utilizador.
form.name_chars_not_allowed=O nome de utilizador "%s" contém caracteres inválidos.
+block.block=Bloquear
+block.block.user=Bloquear utilizador
+block.block.org=Bloquear utilizador para a organização
+block.block.failure=Falhou o bloqueio do utilizador: %s
+block.unblock=Desbloquear
+block.unblock.failure=Falhou o desbloqueio do utilizador: %s
+block.blocked=Bloqueou este utilizador.
+block.title=Bloquear um utilizador
+block.info=Bloquear um utilizador evita que este interaja com repositórios, tal como abrir ou comentar em pedidos de integração ou questões. Saiba mais sobre como bloquear um utilizador.
+block.info_1=Bloquear um utilizador impede as seguintes operações na sua conta e nos seus repositórios:
+block.info_2=seguir a sua conta
+block.info_3=enviar-lhe notificações ao @mencionar o seu nome de utilizador
+block.info_4=convidá-lo/a para ser colaborador/a nos repositórios dele/dela
+block.info_5=juntar aos favoritos, derivar ou vigiar repositórios
+block.info_6=abrir e comentar questões ou pedidos de integração
+block.info_7=reagir aos seus comentários em questões ou pedidos de integração
+block.user_to_block=Utilizador a bloquear
+block.note=Nota
+block.note.title=Nota opcional:
+block.note.info=A nota não é visível para o utilizador bloqueado.
+block.note.edit=Editar nota
+block.list=Utilizadores bloqueados
+block.list.none=Você ainda não bloqueou quaisquer utilizadores.
+
[settings]
profile=Perfil
account=Conta
@@ -757,7 +817,6 @@ gpg_invalid_token_signature=A chave GPG, assinatura ou código fornecidos não c
gpg_token_required=Tem que fornecer uma assinatura para o código abaixo
gpg_token=Código
gpg_token_help=Pode gerar uma assinatura usando o seguinte comando:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Assinatura GPG blindada (com armadura ASCII)
key_signature_gpg_placeholder=Começa com '-----BEGIN PGP SIGNATURE-----'
verify_gpg_key_success=A chave GPG "%s" foi validada.
@@ -950,8 +1009,9 @@ fork_visibility_helper=A visibilidade de um repositório derivado não poderá s
fork_branch=Ramo a ser clonado para a derivação
all_branches=Todos os ramos
fork_no_valid_owners=Não pode fazer uma derivação deste repositório porque não existem proprietários válidos.
+fork.blocked_user=Não pode derivar o repositório porque foi bloqueado/a pelo/a proprietário/a do repositório.
use_template=Usar este modelo
-clone_in_vsc=Clonar no VS Code
+open_with_editor=Abrir com %s
download_zip=Descarregar ZIP
download_tar=Descarregar TAR.GZ
download_bundle=Descarregar PACOTE
@@ -967,6 +1027,8 @@ issue_labels_helper=Escolha um conjunto de rótulos para as questões.
license=Licença
license_helper=Escolha um ficheiro de licença.
license_helper_desc=Uma licença rege o que os outros podem, ou não, fazer com o seu código fonte. Não tem a certeza sobre qual a mais indicada para o seu trabalho? Veja: Escolher uma licença.
+object_format=Formato dos elementos
+object_format_helper=Formato dos elementos do repositório. Não poderá ser alterado mais tarde. SHA1 é o mais compatível.
readme=README
readme_helper=Escolha um modelo de ficheiro README.
readme_helper_desc=Este é o sítio onde pode escrever uma descrição completa do seu trabalho.
@@ -984,6 +1046,7 @@ mirror_prune=Podar
mirror_prune_desc=Remover referências obsoletas de seguimento remoto
mirror_interval=Intervalo entre sincronizações (as unidades de tempo válidas são 'h', 'm' e 's'). O valor zero desabilita a sincronização periódica. (Intervalo mínimo: %s)
mirror_interval_invalid=O intervalo entre sincronizações não é válido.
+mirror_sync=sincronizado
mirror_sync_on_commit=Sincronizar quando forem enviados cometimentos
mirror_address=Clonar a partir do URL
mirror_address_desc=Coloque, na secção de autorização, as credenciais que, eventualmente, sejam necessárias.
@@ -1001,6 +1064,7 @@ watchers=Vigilantes
stargazers=Fãs
stars_remove_warning=Isto irá remover todas as marcas de favoritos deste repositório.
forks=Derivações
+stars=Favoritos
reactions_more=e mais %d
unit_disabled=O administrador desabilitou esta secção do repositório.
language_other=Outros
@@ -1034,6 +1098,7 @@ desc.public=Público
desc.template=Modelo
desc.internal=Interno
desc.archived=Arquivado
+desc.sha256=SHA256
template.items=Itens do modelo
template.git_content=Conteúdo Git (ramo principal)
@@ -1121,6 +1186,7 @@ watch=Vigiar
unstar=Tirar dos favoritos
star=Juntar aos favoritos
fork=Derivar
+action.blocked_user=Não pode realizar a operação porque foi bloqueado/a pelo/a proprietário/a do repositório.
download_archive=Descarregar repositório
more_operations=Mais operações
@@ -1167,6 +1233,8 @@ file_view_rendered=Ver resultado processado
file_view_raw=Ver em bruto
file_permalink=Ligação permanente
file_too_large=O ficheiro é demasiado grande para ser apresentado.
+code_preview_line_from_to=Linhas %[1]d até %[2]d em %[3]s
+code_preview_line_in=Linha %[1]d em %[2]s
invisible_runes_header=`Este ficheiro contém caracteres Unicode invisíveis`
invisible_runes_description=`Este ficheiro contém caracteres Unicode indistinguíveis para humanos mas que podem ser processados de forma diferente por um computador. Se acha que é intencional, pode ignorar este aviso com segurança. Use o botão Revelar para os mostrar.`
ambiguous_runes_header=`Este ficheiro contém caracteres Unicode ambíguos`
@@ -1184,6 +1252,8 @@ audio_not_supported_in_browser=O seu navegador não suporta a etiqueta 'audio' d
stored_lfs=Armazenado com Git LFS
symbolic_link=Ligação simbólica
executable_file=Ficheiro executável
+vendored=Externo
+generated=Gerado
commit_graph=Gráfico de cometimentos
commit_graph.select=Escolher ramos
commit_graph.hide_pr_refs=Ocultar pedidos de integração
@@ -1247,6 +1317,8 @@ editor.file_editing_no_longer_exists=O ficheiro que está a ser editado, "%s", j
editor.file_deleting_no_longer_exists=O ficheiro que está a ser eliminado, "%s", já não existe neste repositório.
editor.file_changed_while_editing=O conteúdo do ficheiro mudou desde que começou a editar. Clique aqui para ver as modificações ou clique em Cometer novamente para escrever por cima.
editor.file_already_exists=Já existe um ficheiro com o nome "%s" neste repositório.
+editor.commit_id_not_matching=O ID do cometimento não corresponde ao ID de quando começou a editar. Faça o cometimento para um ramo de remendo (patch) e depois faça a integração.
+editor.push_out_of_date=O envio parece estar obsoleto.
editor.commit_empty_file_header=Cometer um ficheiro vazio
editor.commit_empty_file_text=O ficheiro que está prestes a cometer está vazio. Quer continuar?
editor.no_changes_to_show=Não existem modificações para mostrar.
@@ -1270,9 +1342,8 @@ commits.desc=Navegar pelo histórico de modificações no código fonte.
commits.commits=Cometimentos
commits.no_commits=Não há cometimentos em comum. "%s" e "%s" têm históricos completamente diferentes.
commits.nothing_to_compare=Estes ramos são iguais.
-commits.search=Procurar cometimentos…
commits.search.tooltip=Pode prefixar palavras-chave com "author:", "committer:", "after:", ou "before:". Por exemplo: "revert author:Alice before:2019-01-13".
-commits.find=Procurar
+commits.search_branch=Este ramo
commits.search_all=Todos os ramos
commits.author=Autor(a)
commits.message=Mensagem
@@ -1323,7 +1394,6 @@ projects.type.basic_kanban=Kanban básico
projects.type.bug_triage=Triagem de erros
projects.template.desc=Modelo de planeamento
projects.template.desc_helper=Escolha um modelo de planeamento para começar
-projects.type.uncategorized=Sem categoria
projects.column.edit=Editar coluna
projects.column.edit_title=Nome
projects.column.new_title=Nome
@@ -1331,8 +1401,6 @@ projects.column.new_submit=Criar coluna
projects.column.new=Nova coluna
projects.column.set_default=Tornar predefinida
projects.column.set_default_desc=Definir esta coluna como a predefinida para questões e pedidos de integração não categorizados
-projects.column.unset_default=Deixar de ser a predefinida
-projects.column.unset_default_desc=Faz com que esta coluna deixe de ser a predefinida
projects.column.delete=Eliminar coluna
projects.column.deletion_desc=Eliminar uma coluna de um planeamento faz com que todas as questões que nela constam sejam movidas para a coluna 'Sem categoria'. Continuar?
projects.column.color=Colorido
@@ -1369,6 +1437,8 @@ issues.new.assignees=Encarregados
issues.new.clear_assignees=Retirar todos os encarregados
issues.new.no_assignees=Sem encarregados
issues.new.no_reviewers=Sem revisores
+issues.new.blocked_user=Não pode criar a questão porque foi bloqueado/a pelo/a proprietário/a do repositório.
+issues.edit.blocked_user=Não pode editar o conteúdo porque foi bloqueado/a pelo/a remetente ou pelo/a proprietário/a do repositório.
issues.choose.get_started=Começar
issues.choose.open_external_link=Abrir
issues.choose.blank=Padrão
@@ -1446,7 +1516,6 @@ issues.filter_sort.moststars=Favorito (decrescente)
issues.filter_sort.feweststars=Favorito (crescente)
issues.filter_sort.mostforks=Mais derivações
issues.filter_sort.fewestforks=Menos derivações
-issues.keyword_search_unavailable=A pesquisa por palavra-chave não está disponível, neste momento. Entre em contacto com o administrador.
issues.action_open=Abrir
issues.action_close=Fechar
issues.action_label=Rótulo
@@ -1484,6 +1553,7 @@ issues.close_comment_issue=Comentar e fechar
issues.reopen_issue=Reabrir
issues.reopen_comment_issue=Comentar e reabrir
issues.create_comment=Comentar
+issues.comment.blocked_user=Não pode criar ou editar o comentário porque foi bloqueado/a pelo remetente ou pelo/a proprietário/a do repositório.
issues.closed_at=`encerrou esta questão %[2]s`
issues.reopened_at=`reabriu esta questão %[2]s`
issues.commit_ref_at=`referenciou esta questão num cometimento %[2]s`
@@ -1682,6 +1752,7 @@ compare.compare_head=comparar
pulls.desc=Habilitar pedidos de integração e revisão de código.
pulls.new=Novo pedido de integração
+pulls.new.blocked_user=Não pode criar o pedido de integração porque foi bloqueado/a pelo/a proprietário/a do repositório.
pulls.view=Ver pedido de integração
pulls.compare_changes=Novo pedido de integração
pulls.allow_edits_from_maintainers=Permitir edições por parte dos responsáveis
@@ -1698,7 +1769,6 @@ pulls.compare_compare=puxar de
pulls.switch_comparison_type=Trocar o tipo de comparação
pulls.switch_head_and_base=Trocar o topo com a base
pulls.filter_branch=Filtrar ramo
-pulls.no_results=Não foram encontrados quaisquer resultados.
pulls.show_all_commits=Mostrar todos os cometimentos
pulls.show_changes_since_your_last_review=Mostrar modificações desde a sua última revisão
pulls.showing_only_single_commit=Mostrando apenas as modificações do comentimento %[1]s
@@ -1707,6 +1777,7 @@ pulls.select_commit_hold_shift_for_range=Escolha o comentimento. Mantenha premid
pulls.review_only_possible_for_full_diff=A revisão só é possível ao visualizar o diff completo
pulls.filter_changes_by_commit=Filtrar por cometimento
pulls.nothing_to_compare=Estes ramos são iguais. Não há necessidade de criar um pedido de integração.
+pulls.nothing_to_compare_have_tag=O ramo/etiqueta escolhidos são iguais.
pulls.nothing_to_compare_and_allow_empty_pr=Estes ramos são iguais. Este pedido de integração ficará vazio.
pulls.has_pull_request=`Já existe um pedido de integração entre estes ramos: %[2]s#%[3]d`
pulls.create=Criar um pedido de integração
@@ -1765,6 +1836,7 @@ pulls.merge_pull_request=Criar um cometimento de integração
pulls.rebase_merge_pull_request=Mudar a base e avançar rapidamente
pulls.rebase_merge_commit_pull_request=Mudar a base e criar um cometimento de integração
pulls.squash_merge_pull_request=Criar cometimento de compactação
+pulls.fast_forward_only_merge_pull_request=Avançar rapidamente apenas
pulls.merge_manually=Integrado manualmente
pulls.merge_commit_id=O ID de cometimento da integração
pulls.require_signed_wont_sign=O ramo requer que os cometimentos sejam assinados mas esta integração não vai ser assinada
@@ -1901,6 +1973,10 @@ wiki.page_name_desc=Insira um nome para esta página Wiki. Alguns dos nomes espe
wiki.original_git_entry_tooltip=Ver o ficheiro Git original, ao invés de usar uma ligação amigável.
activity=Trabalho
+activity.navbar.pulse=Pulso
+activity.navbar.code_frequency=Frequência de programação
+activity.navbar.contributors=Contribuidores
+activity.navbar.recent_commits=Cometimentos recentes
activity.period.filter_label=Período:
activity.period.daily=1 dia
activity.period.halfweekly=3 dias
@@ -1966,16 +2042,10 @@ activity.git_stats_and_deletions=e
activity.git_stats_deletion_1=%d eliminação
activity.git_stats_deletion_n=%d eliminações
-search=Procurar
-search.search_repo=Procurar repositório
-search.type.tooltip=Tipo de pesquisa
-search.fuzzy=Aproximada
-search.fuzzy.tooltip=Incluir também os resultados que estejam próximos do termo de pesquisa
-search.match=Fiel
-search.match.tooltip=Incluir somente os resultados que correspondam rigorosamente ao termo de pesquisa
-search.results=Resultados da procura de "%s" em %s
-search.code_no_results=Não foi encontrado qualquer código-fonte correspondente à sua pesquisa.
-search.code_search_unavailable=A pesquisa por código-fonte não está disponível, neste momento. Entre em contacto com o administrador.
+contributors.contribution_type.filter_label=Tipo de contribuição:
+contributors.contribution_type.commits=Cometimentos
+contributors.contribution_type.additions=Adições
+contributors.contribution_type.deletions=Eliminações
settings=Configurações
settings.desc=Configurações é onde pode gerir as configurações do repositório
@@ -2003,6 +2073,7 @@ settings.mirror_settings.docs.doc_link_title=Como é que eu replico repositório
settings.mirror_settings.docs.doc_link_pull_section=a parte "Puxar de um repositório remoto" da documentação.
settings.mirror_settings.docs.pulling_remote_title=Puxando a partir de um repositório remoto
settings.mirror_settings.mirrored_repository=Repositório replicado
+settings.mirror_settings.pushed_repository=Repositório enviado
settings.mirror_settings.direction=Sentido
settings.mirror_settings.direction.pull=Puxada
settings.mirror_settings.direction.push=Envio
@@ -2024,6 +2095,8 @@ settings.branches.add_new_rule=Adicionar nova regra
settings.advanced_settings=Configurações avançadas
settings.wiki_desc=Habilitar wiki do repositório
settings.use_internal_wiki=Usar o wiki nativo
+settings.default_wiki_branch_name=Nome do ramo predefinido do wiki
+settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki.
settings.use_external_wiki=Usar um wiki externo
settings.external_wiki_url=URL do wiki externo
settings.external_wiki_url_error=O URL do wiki externo não é um URL válido.
@@ -2054,6 +2127,10 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir, por norma, que os
settings.releases_desc=Habilitar lançamentos no repositório
settings.packages_desc=Habilitar o registo de pacotes do repositório
settings.projects_desc=Habilitar planeamentos no repositório
+settings.projects_mode_desc=Modo de planeamentos (tipos de planeamentos a mostrar)
+settings.projects_mode_repo=Apenas planeamentos de repositórios
+settings.projects_mode_owner=Apenas planeamentos de utilizadores ou de organizações
+settings.projects_mode_all=Todos os planeamentos
settings.actions_desc=Habilitar operações no repositório (Gitea Actions)
settings.admin_settings=Configurações do administrador
settings.admin_enable_health_check=Habilitar verificações de integridade (git fsck) no repositório
@@ -2079,6 +2156,7 @@ settings.convert_fork_succeed=A derivação foi convertida num repositório norm
settings.transfer=Transferir a propriedade
settings.transfer.rejected=A transferência do repositório foi rejeitada.
settings.transfer.success=A transferência do repositório foi bem sucedida.
+settings.transfer.blocked_user=Não foi possível transferir o repositório porque foi bloqueado/a pelo/a novo/a proprietário/a.
settings.transfer_abort=Cancelar a transferência
settings.transfer_abort_invalid=Não pode cancelar a transferência de um repositório inexistente.
settings.transfer_abort_success=A transferência de repositório para %s foi cancelada com sucesso.
@@ -2124,11 +2202,11 @@ settings.add_collaborator_success=O colaborador foi adicionado.
settings.add_collaborator_inactive_user=Não é possível adicionar um utilizador desabilitado como colaborador.
settings.add_collaborator_owner=Não é possível adicionar um proprietário como um colaborador.
settings.add_collaborator_duplicate=O colaborador já tinha sido adicionado a este repositório.
+settings.add_collaborator.blocked_user=O/A colaborador/a foi bloqueado/a pelo/a proprietário/a do repositório ou vice-versa.
settings.delete_collaborator=Remover
settings.collaborator_deletion=Remover colaborador
settings.collaborator_deletion_desc=Remover um colaborador irá revogar o seu acesso a este repositório. Quer continuar?
settings.remove_collaborator_success=O colaborador foi removido.
-settings.search_user_placeholder=Procurar utilizador…
settings.org_not_allowed_to_be_collaborator=As organizações não podem ser adicionadas como colaborador.
settings.change_team_access_not_allowed=Alterar o acesso da equipa ao repositório foi restrito ao proprietário da organização
settings.team_not_in_organization=A equipa não está na mesma organização que o repositório
@@ -2136,7 +2214,6 @@ settings.teams=Equipas
settings.add_team=Adicionar equipa
settings.add_team_duplicate=A equipa já tem o repositório
settings.add_team_success=A equipa agora tem acesso ao repositório.
-settings.search_team=Procurar equipa…
settings.change_team_permission_tip=A permissão da equipa é definida na página de configurações da equipa e não pode ter modificações específicas de cada repositório
settings.delete_team_tip=Esta equipa tem acesso a todos os repositórios e não pode ser removida
settings.remove_team_success=O acesso da equipa ao repositório foi removido.
@@ -2289,9 +2366,7 @@ settings.protect_whitelist_committers=Lista de permissões para restringir os en
settings.protect_whitelist_committers_desc=Apenas os utilizadores ou equipas constantes na lista terão permissão para enviar para este ramo (mas não poderão fazer envios forçados).
settings.protect_whitelist_deploy_keys=Dar permissão às chaves de instalação para terem acesso de escrita para enviar.
settings.protect_whitelist_users=Utilizadores com permissão para enviar:
-settings.protect_whitelist_search_users=Procurar utilizadores…
settings.protect_whitelist_teams=Equipas com permissão para enviar:
-settings.protect_whitelist_search_teams=Procurar equipas…
settings.protect_merge_whitelist_committers=Habilitar lista de permissão para integrar
settings.protect_merge_whitelist_committers_desc=Permitir que somente utilizadores ou equipas constantes na lista de permissão possam executar, neste ramo, integrações constantes em pedidos de integração.
settings.protect_merge_whitelist_users=Utilizadores com permissão para executar integrações:
@@ -2312,6 +2387,8 @@ settings.protect_approvals_whitelist_users=Revisores com permissão:
settings.protect_approvals_whitelist_teams=Equipas com permissão para rever:
settings.dismiss_stale_approvals=Descartar aprovações obsoletas
settings.dismiss_stale_approvals_desc=Quando novos cometimentos que mudam o conteúdo do pedido de integração forem enviados para o ramo, as aprovações antigas serão descartadas.
+settings.ignore_stale_approvals=Ignorar aprovações obsoletas
+settings.ignore_stale_approvals_desc=Não contar as aprovações feitas em cometimentos mais antigos (revisões obsoletas) para o número de aprovações do pedido de integração. É irrelevante se as revisões obsoletas já forem descartadas.
settings.require_signed_commits=Exigir cometimentos assinados
settings.require_signed_commits_desc=Rejeitar envios para este ramo que não estejam assinados ou que não sejam validáveis.
settings.protect_branch_name_pattern=Padrão do nome do ramo protegido
@@ -2367,6 +2444,7 @@ settings.archive.error=Ocorreu um erro enquanto decorria o processo de arquivo d
settings.archive.error_ismirror=Não pode arquivar um repositório que tenha sido replicado.
settings.archive.branchsettings_unavailable=As configurações dos ramos não estão disponíveis quando o repositório está arquivado.
settings.archive.tagsettings_unavailable=As configurações sobre etiquetas não estão disponíveis quando o repositório está arquivado.
+settings.archive.mirrors_unavailable=As réplicas não estão disponíveis se o repositório estiver arquivado.
settings.unarchive.button=Desarquivar repositório
settings.unarchive.header=Desarquivar este repositório
settings.unarchive.text=Desarquivar o repositório irá restaurar a capacidade de receber cometimentos e envios, assim como novas questões e pedidos de integração.
@@ -2533,7 +2611,6 @@ branch.default_deletion_failed=O ramo "%s" é o ramo principal, não pode ser el
branch.restore=`Restaurar o ramo "%s"`
branch.download=`Descarregar o ramo "%s"`
branch.rename=`Renomear ramo "%s"`
-branch.search=Pesquisar ramo
branch.included_desc=Este ramo faz parte do ramo principal
branch.included=Incluído
branch.create_new_branch=Criar ramo a partir do ramo:
@@ -2564,6 +2641,16 @@ find_file.no_matching=Não foi encontrado qualquer ficheiro correspondente
error.csv.too_large=Não é possível apresentar este ficheiro por ser demasiado grande.
error.csv.unexpected=Não é possível apresentar este ficheiro porque contém um caractere inesperado na linha %d e coluna %d.
error.csv.invalid_field_count=Não é possível apresentar este ficheiro porque tem um número errado de campos na linha %d.
+error.broken_git_hook=Os automatismos git deste repositório parecem estar danificados. Consulte a documentação sobre como os consertar e depois envie alguns cometimentos para refrescar o estado.
+
+[graphs]
+component_loading=A carregar %s...
+component_loading_failed=Não foi possível carregar %s
+component_loading_info=Isto pode demorar um pouco…
+component_failed_to_load=Ocorreu um erro inesperado.
+code_frequency.what=frequência de programação
+contributors.what=contribuições
+recent_commits.what=cometimentos recentes
[org]
org_name_holder=Nome da organização
@@ -2669,7 +2756,6 @@ teams.write_permission_desc=Esta equipa atribui acesso de escritaadministração: os seus membros podem ler de, enviar para, e adicionar colaboradores aos repositórios da equipa.
teams.create_repo_permission_desc=Adicionalmente, esta equipa atribui a permissão de criar repositórios: os seus membros podem criar novos repositórios na organização.
teams.repositories=Repositórios da equipa
-teams.search_repo_placeholder=Procurar repositório…
teams.remove_all_repos_title=Remover todos os repositórios da equipa
teams.remove_all_repos_desc=Isto irá remover todos os repositórios da equipa.
teams.add_all_repos_title=Adicionar todos os repositórios
@@ -2678,6 +2764,7 @@ teams.add_nonexistent_repo=O repositório que está a tentar adicionar não exis
teams.add_duplicate_users=O utilizador já é um membro da equipa.
teams.repos.none=Não há repositórios que possam ser acedidos por esta equipa.
teams.members.none=Não há membros nesta equipa.
+teams.members.blocked_user=Não foi possível adicionar o/a utilizador/a porque essa operação foi bloqueada pela organização.
teams.specific_repositories=Repositórios específicos
teams.specific_repositories_helper=Os membros só terão acesso a repositórios explicitamente adicionados à equipa. Escolher isto não irá remover automaticamente os repositórios já adicionados com Todos os repositórios.
teams.all_repositories=Todos os repositórios
@@ -2690,7 +2777,9 @@ teams.invite.by=Convidado(a) por %s
teams.invite.description=Clique no botão abaixo para se juntar à equipa.
[admin]
+maintenance=Manutenção
dashboard=Painel de controlo
+self_check=Auto-verificação
identity_access=Identidade e acesso
users=Contas de utilizador
organizations=Organizações
@@ -2701,6 +2790,8 @@ integrations=Integrações
authentication=Fontes de autenticação
emails=Emails do utilizador
config=Configuração
+config_summary=Resumo
+config_settings=Configurações
notices=Notificações do sistema
monitor=Monitorização
first_page=Primeira
@@ -2710,7 +2801,7 @@ settings=Configurações de administração
dashboard.new_version_hint=O Gitea %s está disponível, você está a correr a versão %s. Verifique o blog para mais detalhes.
dashboard.statistic=Resumo
-dashboard.operations=Operações de manutenção
+dashboard.maintenance_operations=Operações de manutenção
dashboard.system_status=Estado do sistema
dashboard.operation_name=Nome da operação
dashboard.operation_switch=Comutar
@@ -2736,6 +2827,7 @@ dashboard.delete_missing_repos=Eliminar todos os repositórios que não tenham o
dashboard.delete_missing_repos.started=Foi iniciada a tarefa de eliminação de todos os repositórios que não têm ficheiros git.
dashboard.delete_generated_repository_avatars=Eliminar avatares gerados do repositório
dashboard.sync_repo_branches=Sincronizar ramos perdidos de dados do git para bases de dados
+dashboard.sync_repo_tags=Sincronizar etiquetas dos dados do git para a base de dados
dashboard.update_mirrors=Sincronizar réplicas
dashboard.repo_health_check=Verificar a saúde de todos os repositórios
dashboard.check_repo_stats=Verificar as estatísticas de todos os repositórios
@@ -2790,6 +2882,7 @@ dashboard.stop_endless_tasks=Parar tarefas intermináveis
dashboard.cancel_abandoned_jobs=Cancelar trabalhos abandonados
dashboard.start_schedule_tasks=Iniciar tarefas de agendamento
dashboard.sync_branch.started=Sincronização de ramos iniciada
+dashboard.sync_tag.started=Sincronização de etiquetas iniciada
dashboard.rebuild_issue_indexer=Reconstruir indexador de questões
users.user_manage_panel=Gestão das contas de utilizadores
@@ -2875,9 +2968,6 @@ repos.unadopted.no_more=Não foram encontrados mais repositórios não adoptados
repos.owner=Proprietário(a)
repos.name=Nome
repos.private=Privado
-repos.watches=Vigilâncias
-repos.stars=Favoritos
-repos.forks=Derivações
repos.issues=Questões
repos.size=Tamanho
repos.lfs_size=Tamanho do LFS
@@ -3002,7 +3092,7 @@ auths.tip.nextcloud=`Registe um novo consumidor OAuth na sua instância usando o
auths.tip.dropbox=Crie uma nova aplicação em https://www.dropbox.com/developers/apps
auths.tip.facebook=`Registe uma nova aplicação em https://developers.facebook.com/apps e adicione o produto "Facebook Login"`
auths.tip.github=Registe uma nova aplicação OAuth em https://github.com/settings/applications/new
-auths.tip.gitlab=Registe uma nova aplicação em https://gitlab.com/profile/applications
+auths.tip.gitlab_new=Registe uma nova aplicação em https://gitlab.com/-/profile/applications
auths.tip.google_plus=Obtenha credenciais de cliente OAuth2 a partir da consola do Google API em https://console.developers.google.com/
auths.tip.openid_connect=Use o URL da descoberta de conexão OpenID (/.well-known/openid-configuration) para especificar os extremos
auths.tip.twitter=`Vá a https://dev.twitter.com/apps, crie uma aplicação e certifique-se de que está habilitada a opção "Allow this application to be used to Sign in with Twitter"`
@@ -3138,6 +3228,7 @@ config.picture_config=Configuração da imagem e do avatar
config.picture_service=Serviço de imagem
config.disable_gravatar=Desabilitar o Gravatar
config.enable_federated_avatar=Habilitar avatares federados
+config.open_with_editor_app_help=Os editores de "Abrir com" do menu de clonagem. Se for deixado em branco, será usado o predefinido. Expanda para ver o predefinido.
config.git_config=Configuração Git
config.git_disable_diff_highlight=Desabilitar o realce de sintaxe no diff
@@ -3216,6 +3307,14 @@ notices.desc=Descrição
notices.op=Op.
notices.delete_success=As notificações do sistema foram eliminadas.
+self_check.no_problem_found=Nenhum problema encontrado até agora.
+self_check.startup_warnings=Alertas do arranque:
+self_check.database_collation_mismatch=Supor que a base de dados usa a colação: %s
+self_check.database_collation_case_insensitive=A base de dados está a usar a colação %s, que é insensível à diferença entre maiúsculas e minúsculas. Embora o Gitea possa trabalhar com ela, pode haver alguns casos raros que não funcionem como esperado.
+self_check.database_inconsistent_collation_columns=A base de dados está a usar a colação %s, mas estas colunas estão a usar colações diferentes. Isso poderá causar alguns problemas inesperados.
+self_check.database_fix_mysql=Para utilizadores do MySQL/MariaDB, pode usar o comando "gitea doctor convert" para resolver os problemas de colação. Também pode resolver o problema com comandos SQL "ALTER ... COLLATE ..." aplicados manualmente.
+self_check.database_fix_mssql=Para utilizadores do MSSQL só pode resolver o problema aplicando comandos SQL "ALTER ... COLLATE ..." manualmente, por enquanto.
+
[action]
create_repo=criou o repositório %s
rename_repo=renomeou o repositório de %[1]s
para %[3]s
@@ -3400,6 +3499,9 @@ rpm.registry=Configurar este registo usando a linha de comandos:
rpm.distros.redhat=em distribuições baseadas no RedHat
rpm.distros.suse=em distribuições baseadas no SUSE
rpm.install=Para instalar o pacote, execute o seguinte comando:
+rpm.repository=Informação do repositório
+rpm.repository.architectures=Arquitecturas
+rpm.repository.multiple_groups=Este pacote está disponível em vários grupos.
rubygems.install=Para instalar o pacote usando o gem, execute o seguinte comando:
rubygems.install2=ou adicione-o ao ficheiro Gemfile
:
rubygems.dependencies.runtime=Dependências do tempo de execução (runtime)
@@ -3526,14 +3628,15 @@ runs.scheduled=Agendadas
runs.pushed_by=enviado por
runs.invalid_workflow_helper=O ficheiro de configuração da sequência de trabalho é inválido. Verifique o seu ficheiro de configuração: %s
runs.no_matching_online_runner_helper=Não existem executores ligados que tenham o rótulo %s
+runs.no_job_without_needs=A sequência de trabalho tem que conter pelo menos um trabalho sem dependências.
runs.actor=Interveniente
runs.status=Estado
runs.actors_no_select=Todos os intervenientes
runs.status_no_select=Todos os estados
runs.no_results=Nenhum resultado obtido.
runs.no_workflows=Ainda não há sequências de trabalho.
-runs.no_workflows.quick_start=Não sabe como começar com o Gitea Action? Veja o guia de iniciação rápida.
-runs.no_workflows.documentation=Para mais informação sobre o Gitea Action, veja a documentação.
+runs.no_workflows.quick_start=Não sabe como começar com o Gitea Actions? Veja o guia de inicio rápido.
+runs.no_workflows.documentation=Para mais informação sobre o Gitea Actions veja a documentação.
runs.no_runs=A sequência de trabalho ainda não foi executada.
runs.empty_commit_message=(mensagem de cometimento vazia)
@@ -3552,7 +3655,7 @@ variables.none=Ainda não há variáveis.
variables.deletion=Remover variável
variables.deletion.description=Remover uma variável é permanente e não pode ser revertido. Quer continuar?
variables.description=As variáveis serão transmitidas a certas operações e não poderão ser lidas de outra forma.
-variables.id_not_exist=A variável com o id %d não existe.
+variables.id_not_exist=A variável com o ID %d não existe.
variables.edit=Editar variável
variables.deletion.failed=Falha ao remover a variável.
variables.deletion.success=A variável foi removida.
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index 0a466854d0..818dad1147 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -139,6 +139,15 @@ confirm_delete_selected=Вы уверены, что хотите удалить
name=Название
value=Значение
+filter=Фильтр
+filter.is_archived=Архивировано
+filter.is_template=Шаблон
+filter.public=Публичный
+filter.private=Личный
+
+
+[search]
+
[aria]
navbar=Панель навигации
footer=Подвал
@@ -312,7 +321,6 @@ collaborative_repos=Совместные репозитории
my_orgs=Мои организации
my_mirrors=Мои зеркала
view_home=Показать %s
-search_repos=Поиск репозитория…
filter=Другие фильтры
filter_by_team_repositories=Фильтровать по репозиториям команды
feed_of=Лента «%s»
@@ -333,20 +341,8 @@ issues.in_your_repos=В ваших репозиториях
repos=Репозитории
users=Пользователи
organizations=Организации
-search=Поиск
go_to=Перейти к
code=Код
-search.type.tooltip=Тип поиска
-search.fuzzy=Неточный
-search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу
-search.match=Соответствие
-search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу
-code_search_unavailable=В настоящее время поиск по коду недоступен. Обратитесь к администратору сайта.
-repo_no_results=Подходящие репозитории не найдены.
-user_no_results=Подходящие пользователи не найдены.
-org_no_results=Подходящие организации не найдены.
-code_no_results=Соответствующий поисковому запросу исходный код не найден.
-code_search_results=Результаты поиска «%s»
code_last_indexed_at=Последний проиндексированный %s
relevant_repositories_tooltip=Репозитории, являющиеся ответвлениями или не имеющие ни темы, ни значка, ни описания, скрыты.
relevant_repositories=Показаны только релевантные репозитории, показать результаты без фильтрации.
@@ -364,7 +360,6 @@ forgot_password_title=Восстановить пароль
forgot_password=Забыли пароль?
sign_up_now=Нужен аккаунт? Зарегистрируйтесь.
sign_up_successful=Учётная запись успешно создана. Добро пожаловать!
-confirmation_mail_sent_prompt=Новое письмо для подтверждения направлено на %s. Пожалуйста, проверьте ваш почтовый ящик в течение %s для завершения регистрации.
must_change_password=Обновить пароль
allow_password_change=Требовать смену пароля пользователем (рекомендуется)
reset_password_mail_sent_prompt=Письмо с подтверждением отправлено на %s. Пожалуйста, проверьте входящую почту в течение %s, чтобы завершить процесс восстановления аккаунта.
@@ -586,6 +581,7 @@ org_still_own_packages=Эта организация всё ещё владее
target_branch_not_exist=Целевая ветка не существует.
+
[user]
change_avatar=Изменить свой аватар…
joined_on=Присоединил(ся/ась) %s
@@ -611,6 +607,7 @@ form.name_reserved=Имя пользователя «%s» зарезервиро
form.name_pattern_not_allowed=Шаблон «%s» не допускается в имени пользователя.
form.name_chars_not_allowed=Имя пользователя «%s» содержит недопустимые символы.
+
[settings]
profile=Профиль
account=Аккаунт
@@ -754,7 +751,6 @@ gpg_invalid_token_signature=Предоставленный ключ GPG, под
gpg_token_required=Вы должны предоставить подпись для токена ниже
gpg_token=Токен
gpg_token_help=Вы можете сгенерировать подпись с помощью:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Текстовая подпись GPG
key_signature_gpg_placeholder=Начинается с '-----BEGIN PGP SIGNATURE-----'
verify_gpg_key_success=Ключ GPG «%s» верифицирован.
@@ -941,7 +937,6 @@ fork_visibility_helper=Видимость форкнутого репозито
fork_branch=Ветка для клонирования в форк
all_branches=Все ветки
use_template=Использовать этот шаблон
-clone_in_vsc=Клонировать в VS Code
download_zip=Скачать ZIP
download_tar=Скачать TAR.GZ
download_bundle=Скачать BUNDLE
@@ -1248,9 +1243,7 @@ commits.desc=Просмотр истории изменений исходног
commits.commits=Коммитов
commits.no_commits=Нет общих коммитов. «%s» и «%s» имеют совершенно разные истории.
commits.nothing_to_compare=Эти ветки одинаковы.
-commits.search=Поиск коммитов…
commits.search.tooltip=Можно предварять ключевые слова префиксами "author:", "committer:", "after:", или "before:", например "revert author:Alice before:2019-01-13".
-commits.find=Поиск
commits.search_all=Все ветки
commits.author=Автор
commits.message=Сообщение
@@ -1300,7 +1293,6 @@ projects.type.basic_kanban=Обычный Канбан
projects.type.bug_triage=Планирование работы с багами
projects.template.desc=Шаблон проекта
projects.template.desc_helper=Выберите шаблон проекта для начала
-projects.type.uncategorized=Без категории
projects.column.edit=Изменить столбец
projects.column.edit_title=Название
projects.column.new_title=Название
@@ -1308,10 +1300,7 @@ projects.column.new_submit=Создать столбец
projects.column.new=Новый столбец
projects.column.set_default=Установить по умолчанию
projects.column.set_default_desc=Назначить этот столбец по умолчанию для неклассифицированных задач и запросов на слияние
-projects.column.unset_default=Снять установку по умолчанию
-projects.column.unset_default_desc=Снять установку этого столбца по умолчанию
projects.column.delete=Удалить столбец
-projects.column.deletion_desc=При удалении столбца проекта все связанные задачи перемещаются в 'Без категории'. Продолжить?
projects.column.color=Цвет
projects.open=Открыть
projects.close=Закрыть
@@ -1423,7 +1412,6 @@ issues.filter_sort.moststars=Больше звезд
issues.filter_sort.feweststars=Меньше звезд
issues.filter_sort.mostforks=Больше форков
issues.filter_sort.fewestforks=Меньше форков
-issues.keyword_search_unavailable=В настоящее время поиск по ключевым словам недоступен. Обратитесь к администратору сайта.
issues.action_open=Открыть
issues.action_close=Закрыть
issues.action_label=Метка
@@ -1672,7 +1660,6 @@ pulls.compare_compare=взять из
pulls.switch_comparison_type=Переключить тип сравнения
pulls.switch_head_and_base=Поменять исходную и целевую ветки местами
pulls.filter_branch=Фильтр по ветке
-pulls.no_results=Результатов не найдено.
pulls.show_all_commits=Показать все коммиты
pulls.show_changes_since_your_last_review=Показать изменения с момента вашего последнего отзыва
pulls.showing_only_single_commit=Показать только изменения коммита %[1]s
@@ -1926,16 +1913,7 @@ activity.git_stats_and_deletions=и
activity.git_stats_deletion_1=%d удаление
activity.git_stats_deletion_n=%d удалений
-search=Поиск
-search.search_repo=Поиск по репозиторию
-search.type.tooltip=Тип поиска
-search.fuzzy=Неточный
-search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу
-search.match=Соответствие
-search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу
-search.results=Результаты поиска "%s" в %s
-search.code_no_results=Не найдено исходного кода, соответствующего поисковому запросу.
-search.code_search_unavailable=В настоящее время поиск по коду недоступен. Обратитесь к администратору сайта.
+contributors.contribution_type.commits=коммитов
settings=Настройки
settings.desc=В настройках вы можете менять различные параметры этого репозитория
@@ -2012,6 +1990,7 @@ settings.pulls.default_allow_edits_from_maintainers=По умолчанию ра
settings.releases_desc=Включить релизы
settings.packages_desc=Включить реестр пакетов
settings.projects_desc=Включить проекты репозитория
+settings.projects_mode_all=Все проекты
settings.actions_desc=Включить действия репозитория
settings.admin_settings=Настройки администратора
settings.admin_enable_health_check=Выполнять проверки целостности этого репозитория (git fsck)
@@ -2086,7 +2065,6 @@ settings.delete_collaborator=Удалить
settings.collaborator_deletion=Удалить соавтора
settings.collaborator_deletion_desc=Этот пользователь больше не будет иметь доступа для совместной работы в этом репозитории после удаления. Вы хотите продолжить?
settings.remove_collaborator_success=Соавтор удалён.
-settings.search_user_placeholder=Поиск пользователя…
settings.org_not_allowed_to_be_collaborator=Организации не могут быть добавлены как соавторы.
settings.change_team_access_not_allowed=Доступ к репозиторию команде ограничен владельцем организации
settings.team_not_in_organization=Команда не в той же организации, что и репозиторий
@@ -2094,7 +2072,6 @@ settings.teams=Команды
settings.add_team=Добавить команду
settings.add_team_duplicate=Команда уже имеет репозиторий
settings.add_team_success=Команда теперь имеет доступ к репозиторию.
-settings.search_team=Поиск команды…
settings.change_team_permission_tip=Разрешение команды установлено на странице настройки команды и не может быть изменено для каждого репозитория
settings.delete_team_tip=Эта команда имеет доступ ко всем репозиториям и не может быть удалена
settings.remove_team_success=Доступ команды к репозиторию удалён.
@@ -2245,9 +2222,7 @@ settings.protect_whitelist_committers=Ограничение отправки п
settings.protect_whitelist_committers_desc=Только пользователям или командам из белого списка будет разрешена отправка изменений в эту ветку (но не принудительная отправка).
settings.protect_whitelist_deploy_keys=Белый список развёртываемых ключей с доступом на запись в push.
settings.protect_whitelist_users=Пользователи, которые могут отправлять изменения в эту ветку:
-settings.protect_whitelist_search_users=Поиск пользователей…
settings.protect_whitelist_teams=Команды, члены которых могут отправлять изменения в эту ветку:
-settings.protect_whitelist_search_teams=Поиск команд…
settings.protect_merge_whitelist_committers=Ограничить право на слияние белым списком
settings.protect_merge_whitelist_committers_desc=Разрешить принимать запросы на слияние в эту ветку только пользователям и командам из «белого списка».
settings.protect_merge_whitelist_users=Пользователи с правом на слияние:
@@ -2483,7 +2458,6 @@ branch.default_deletion_failed=Ветка «%s» является веткой
branch.restore=Восстановить ветку «%s»
branch.download=Скачать ветку «%s»
branch.rename=Переименовать ветку «%s»
-branch.search=Поиск ветки
branch.included_desc=Эта ветка является частью ветки по умолчанию
branch.included=Включено
branch.create_new_branch=Создать ветку из ветви:
@@ -2515,6 +2489,8 @@ error.csv.too_large=Не удается отобразить этот файл,
error.csv.unexpected=Не удается отобразить этот файл, потому что он содержит неожиданный символ в строке %d и столбце %d.
error.csv.invalid_field_count=Не удается отобразить этот файл, потому что он имеет неправильное количество полей в строке %d.
+[graphs]
+
[org]
org_name_holder=Название организации
org_full_name_holder=Полное название организации
@@ -2618,7 +2594,6 @@ teams.write_permission_desc=Эта команда предоставляет д
teams.admin_permission_desc=Эта команда даёт административный доступ: участники могут читать, отправлять изменения и добавлять соавторов к её репозиториям.
teams.create_repo_permission_desc=Кроме того, эта команда предоставляет право Создание репозитория: члены команды могут создавать новые репозитории в организации.
teams.repositories=Репозитории группы разработки
-teams.search_repo_placeholder=Поиск репозитория…
teams.remove_all_repos_title=Удалить все репозитории команды
teams.remove_all_repos_desc=Удаляет все репозитории из команды.
teams.add_all_repos_title=Добавить все репозитории
@@ -2649,6 +2624,8 @@ integrations=Интеграции
authentication=Аутентификация
emails=Адреса эл. почты пользователей
config=Конфигурация
+config_summary=Статистика
+config_settings=Настройки
notices=Системные уведомления
monitor=Мониторинг
first_page=Первая
@@ -2657,7 +2634,6 @@ total=Всего: %d
dashboard.new_version_hint=Доступна новая версия Gitea %s, вы используете %s. Более подробную информацию читайте в блоге.
dashboard.statistic=Статистика
-dashboard.operations=Операции
dashboard.system_status=Состояние системы
dashboard.operation_name=Имя операции
dashboard.operation_switch=Переключить
@@ -2817,9 +2793,6 @@ repos.unadopted.no_more=Больше непринятых репозиторие
repos.owner=Владелец
repos.name=Название
repos.private=Личный
-repos.watches=Следят
-repos.stars=Звезды
-repos.forks=Форки
repos.issues=Задачи
repos.size=Размер
repos.lfs_size=Размер LFS
@@ -2941,7 +2914,6 @@ auths.tip.nextcloud=`Зарегистрируйте нового потреби
auths.tip.dropbox=Добавьте новое приложение на https://www.dropbox.com/developers/apps
auths.tip.facebook=Зарегистрируйте новое приложение на https://developers.facebook.com/apps и добавьте модуль «Facebook Login»
auths.tip.github=Добавьте OAuth приложение на https://github.com/settings/applications/new
-auths.tip.gitlab=Добавьте новое приложение на https://gitlab.com/profile/applications
auths.tip.google_plus=Получите учётные данные клиента OAuth2 в консоли Google API на странице https://console.developers.google.com/
auths.tip.openid_connect=Используйте OpenID Connect Discovery URL (/.well-known/openid-configuration) для автоматической настройки входа OAuth
auths.tip.twitter=Перейдите на https://dev.twitter.com/apps, создайте приложение и убедитесь, что включена опция «Разрешить это приложение для входа в систему с помощью Twitter»
@@ -3153,6 +3125,7 @@ notices.desc=Описание
notices.op=Oп.
notices.delete_success=Уведомления системы были удалены.
+
[action]
create_repo=создал(а) репозиторий %s
rename_repo=переименовал(а) репозиторий из %[1]s
на %[3]s
@@ -3337,6 +3310,8 @@ rpm.registry=Настроить реестр из командной строк
rpm.distros.redhat=на дистрибутивах семейства RedHat
rpm.distros.suse=на дистрибутивах семейства SUSE
rpm.install=Чтобы установить пакет, выполните следующую команду:
+rpm.repository=О репозитории
+rpm.repository.architectures=Архитектуры
rubygems.install=Чтобы установить пакет с помощью gem, выполните следующую команду:
rubygems.install2=или добавьте его в Gemfile:
rubygems.dependencies.runtime=Зависимости времени выполнения
@@ -3464,8 +3439,6 @@ runs.status=Статус
runs.actors_no_select=Все акторы
runs.no_results=Ничего не найдено.
runs.no_workflows=Пока нет рабочих процессов.
-runs.no_workflows.quick_start=Не знаете, как начать использовать Действия Gitea? Читайте руководство по быстрому старту.
-runs.no_workflows.documentation=Чтобы узнать больше о Действиях Gitea, читайте документацию.
runs.no_runs=Рабочий поток ещё не запускался.
runs.empty_commit_message=(пустое сообщение коммита)
@@ -3484,7 +3457,6 @@ variables.none=Переменных пока нет.
variables.deletion=Удалить переменную
variables.deletion.description=Удаление переменной необратимо, его нельзя отменить. Продолжить?
variables.description=Переменные будут передаваться определенным действиям и не могут быть прочитаны иначе.
-variables.id_not_exist=Переменная с идентификатором %d не существует.
variables.edit=Изменить переменную
variables.deletion.failed=Не удалось удалить переменную.
variables.deletion.success=Переменная удалена.
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 6d70bc385a..99559802c5 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -100,6 +100,15 @@ concept_user_organization=සංවිධානය
name=නම
+filter=පෙරහන
+filter.is_archived=සංරක්ෂිත
+filter.is_template=සැකිලි
+filter.public=ප්රසිද්ධ
+filter.private=පෞද්ගලික
+
+
+[search]
+
[aria]
[heatmap]
@@ -223,7 +232,6 @@ collaborative_repos=සහයෝගී ගබඩාවලදී
my_orgs=මාගේ සංවිධාන
my_mirrors=මගේ දර්පණ
view_home=%s දකින්න
-search_repos=ගබඩාවක් සොයා ගන්න…
filter=වෙනත් පෙරහන්
filter_by_team_repositories=කණ්ඩායම් කෝෂ්ඨ අනුව පෙරන්න
@@ -243,13 +251,7 @@ issues.in_your_repos=ඔබගේ කෝෂ්ඨවල
repos=කෝෂ්ඨ
users=පරිශීලකයින්
organizations=සංවිධාන
-search=සොයන්න
code=කේතය
-search.match=තරගය
-repo_no_results=ගැලපෙන ගබඩාවක් හමු නොවීය.
-user_no_results=ගැලපෙන පරිශීලකයින් හමු නොවීය.
-org_no_results=ගැලපෙන සංවිධාන හමු නොවීය.
-code_no_results=ඔබගේ සෙවුම් පදය ගැලපෙන ප්රභව කේතයක් නොමැත.
code_last_indexed_at=අවසන් සුචිගත %s
[auth]
@@ -262,7 +264,6 @@ remember_me=උපාංගය මතක තබාගන්න
forgot_password_title=මුරපදය අමතක වුණා
forgot_password=මුරපදය අමතක වුණා ද?
sign_up_now=ගිණුමක් ඇවැසිද? දැන් ලියාපදිංචි වන්න.
-confirmation_mail_sent_prompt=නව තහවුරු කිරීමේ විද්යුත් තැපෑලක් %sවෙත යවා ඇත. ලියාපදිංචි කිරීමේ ක්රියාවලිය සම්පූර්ණ කිරීම සඳහා කරුණාකර ඊළඟ %s තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න.
must_change_password=මුරපදය යාවත්කාල කරන්න
allow_password_change=මුරපදය වෙනස් කිරීමට පරිශීලකයාට අවශ්ය වේ (නිර්දේශිත)
reset_password_mail_sent_prompt=තහවුරු කිරීමේ විද්යුත් තැපෑලක් %sවෙත යවා ඇත. ඊළඟ තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න %s ගිණුම යථා ක්රියාවලිය සම්පූර්ණ කිරීම සඳහා.
@@ -450,6 +451,7 @@ auth_failed=සත්යාපන අසමත් විය: %v
target_branch_not_exist=ඉලක්කගත ශාඛාව නොපවතී.
+
[user]
change_avatar=ඔබගේ අවතාරය වෙනස් කරන්න…
repositories=කෝෂ්ඨ
@@ -466,6 +468,7 @@ user_bio=චරිතාපදානය
disabled_public_activity=මෙම පරිශීලකයා ක්රියාකාරකම්වල මහජන දෘශ්යතාව අක්රීය කර ඇත.
+
[settings]
profile=පැතිකඩ
account=ගිණුම
@@ -577,7 +580,6 @@ gpg_invalid_token_signature=සපයන ලද GPG යතුර, අත්ස
gpg_token_required=පහත ටෝකනය සඳහා ඔබ අත්සනක් ලබා දිය යුතුය
gpg_token=ටෝකනය
gpg_token_help=ඔබට අත්සනක් ජනනය කළ හැකිය:
-gpg_token_code=දෝංකාරය "%s" | gpg -a -පැහැර හැරීම-යතුර %s —වෙන්ච-සිග්
gpg_token_signature=සන්නද්ධ GPG අත්සන
key_signature_gpg_placeholder=ආරම්භ වන්නේ '—ආරම්භ කරන්න PGP සිග්නේටුර්—'
ssh_key_verified=සත්යාපිත යතුර
@@ -716,7 +718,6 @@ fork_repo=දෙබලක ගබඩාව
fork_from=සිට දෙබලක
fork_visibility_helper=ව්යාජ ගබඩාවේ දෘශ්යතාව වෙනස් කළ නොහැක.
use_template=මෙම අච්චුව භාවිතා කරන්න
-clone_in_vsc=VS කේතය පරිගණක ක්රිඩාවට සමාන
download_zip=ZIP බාගන්න
download_tar=TAR.GZ බාගන්න
download_bundle=බණ්ඩලය බාගත කරන්න
@@ -942,8 +943,6 @@ editor.require_signed_commit=ශාඛාවට අත්සන් කළ කැ
commits.desc=මූලාශ්ර කේත වෙනස් කිරීමේ ඉතිහාසය පිරික්සන්න.
commits.commits=විවරයන්
commits.nothing_to_compare=මෙම ශාඛා සමාන වේ.
-commits.search=සෙවුම් වාර…
-commits.find=සොයන්න
commits.search_all=සියළුම ශාඛා
commits.author=කතෘ
commits.message=පණිවිඩය
@@ -980,7 +979,6 @@ projects.type.basic_kanban=මූලික කන්ෙවනි
projects.type.bug_triage=දෝෂ ට්රයිජ්
projects.template.desc=ව්යාපෘති සැකිල්ල
projects.template.desc_helper=ආරම්භ කිරීම සඳහා ව්යාපෘති සැකිල්ලක් තෝරන්න
-projects.type.uncategorized=ප්රවර්ග ගත නැති
projects.column.edit_title=නම
projects.column.new_title=නම
projects.column.color=වර්ණය
@@ -1262,7 +1260,6 @@ pulls.compare_compare=සිට අදින්න
pulls.switch_comparison_type=ස්විච් සංසන්දනය වර්ගය
pulls.switch_head_and_base=හිස සහ පාදය මාරු කරන්න
pulls.filter_branch=ශාඛාව පෙරන්න
-pulls.no_results=ප්රතිඵල සොයාගත නොහැකි විය.
pulls.nothing_to_compare=මෙම ශාඛා සමාන වේ. අදින්න ඉල්ලීමක් නිර්මාණය කිරීමට අවශ්ය නැත.
pulls.nothing_to_compare_and_allow_empty_pr=මෙම ශාඛා සමාන වේ. මෙම මහජන සම්බන්ධතා හිස් වනු ඇත.
pulls.has_pull_request=`මෙම ශාඛා අතර අදින්න ඉල්ලීම දැනටමත් පවතී: %[2]s #%[3]d`
@@ -1459,12 +1456,7 @@ activity.git_stats_and_deletions=සහ
activity.git_stats_deletion_1=%d මකාදැමීම
activity.git_stats_deletion_n=%d මකාදැමීම්
-search=සොයන්න
-search.search_repo=කෝෂ්ඨය සොයන්න
-search.fuzzy=සිනිඳු
-search.match=තරගය
-search.results=%s හි "%s" සඳහා සෙවුම් ප්රතිඵල
-search.code_no_results=ඔබගේ සෙවුම් පදය ගැලපෙන ප්රභව කේතයක් නොමැත.
+contributors.contribution_type.commits=විවරයන්
settings=සැකසුම්
settings.desc=සැකසුම් යනු ගබඩාව සඳහා සැකසුම් කළමනාකරණය කළ හැකි ස්ථානයයි
@@ -1581,7 +1573,6 @@ settings.delete_collaborator=ඉවත් කරන්න
settings.collaborator_deletion=සහයෝගිතාකරු ඉවත් කරන්න
settings.collaborator_deletion_desc=සහයෝගිතාකරුවෙකු ඉවත් කිරීම මෙම ගබඩාවට ඔවුන්ගේ ප්රවේශය අවලංගු කරනු ඇත. දිගටම?
settings.remove_collaborator_success=සහයෝගිතාකරු ඉවත් කර ඇත.
-settings.search_user_placeholder=පරිශීලක සොයන්න…
settings.org_not_allowed_to_be_collaborator=සහයෝගීකයෙකු ලෙස සංවිධාන එකතු කළ නොහැක.
settings.change_team_access_not_allowed=ගබඩාව සඳහා කණ්ඩායම් ප්රවේශය වෙනස් කිරීම සංවිධාන හිමිකරුට සීමා කර ඇත
settings.team_not_in_organization=මෙම කණ්ඩායම ගබඩාවේ එකම සංවිධානයේ නොමැත
@@ -1589,7 +1580,6 @@ settings.teams=කණ්ඩායම්
settings.add_team=කණ්ඩායම එකතු කරන්න
settings.add_team_duplicate=කණ්ඩායම දැනටමත් ගබඩාවක් ඇත
settings.add_team_success=කණ්ඩායමට දැන් කෝෂ්ඨයට ප්රවේශය ඇත.
-settings.search_team=කණ්ඩායම සොයන්න…
settings.change_team_permission_tip=කණ්ඩායමේ අවසරය කණ්ඩායම් සැකසුම් පිටුවේ සකසන අතර කෝෂ්ඨය අනුව වෙනස් කළ නොහැකිය
settings.delete_team_tip=මෙම කණ්ඩායම සියළුම කෝෂ්ඨවලට ප්රවේශය ඇති අතර ඉවත් කළ නොහැකිය
settings.remove_team_success=කෝෂ්ඨය වෙත කණ්ඩායමේ ප්රවේශය ඉවත් කර ඇත.
@@ -1706,9 +1696,7 @@ settings.protect_whitelist_committers=වයිට්ලිස්ට් සී
settings.protect_whitelist_committers_desc=මෙම ශාඛාව වෙත තල්ලු කිරීමට අවසර ඇත්තේ වයිට්ලිස්ට් පරිශීලකයින්ට හෝ කණ්ඩායම්වලට පමණි (නමුත් බල තල්ලුව නොවේ).
settings.protect_whitelist_deploy_keys=වයිට්ලිස්ට් තල්ලු කිරීමට ලිවීමේ ප්රවේශය සහිත යතුරු යොදවන්න.
settings.protect_whitelist_users=තල්ලු කිරීම සඳහා වයිට්ලිස්ට් පරිශීලකයින්:
-settings.protect_whitelist_search_users=පරිශීලකයින් සොයන්න…
settings.protect_whitelist_teams=තල්ලු කිරීම සඳහා වයිට්ලිස්ට් කණ්ඩායම්:
-settings.protect_whitelist_search_teams=කණ්ඩායම් සොයන්න…
settings.protect_merge_whitelist_committers=ඒකාබද්ධ වයිට්ලිස්ට් සක්රීය කරන්න
settings.protect_merge_whitelist_committers_desc=මෙම ශාඛාවට ඇද ගැනීමේ ඉල්ලීම් ඒකාබද්ධ කිරීමට සුදු පැහැති පරිශීලකයින්ට හෝ කණ්ඩායම්වලට පමණක් ඉඩ දෙන්න.
settings.protect_merge_whitelist_users=ඒකාබද්ධ කිරීම සඳහා Whitelisted පරිශීලකයන්:
@@ -1910,6 +1898,8 @@ error.csv.too_large=එය ඉතා විශාල නිසා මෙම ග
error.csv.unexpected=%d පේළියේ සහ %dතීරුවේ අනපේක්ෂිත චරිතයක් අඩංගු බැවින් මෙම ගොනුව විදැහුම්කරණය කළ නොහැක.
error.csv.invalid_field_count=මෙම ගොනුව රේඛාවේ වැරදි ක්ෂේත්ර සංඛ්යාවක් ඇති බැවින් එය විදැහුම්කරණය කළ නොහැක %d.
+[graphs]
+
[org]
org_name_holder=සංවිධානයේ නම
org_full_name_holder=සංවිධානයේ සම්පූර්ණ නම
@@ -2001,7 +1991,6 @@ teams.write_permission_desc=මෙම කණ්ඩායම ප්රදාන
teams.admin_permission_desc=මෙම කණ්ඩායම පරිපාලක ප්රවේශය ලබා දෙයි: සාමාජිකයින්ට කියවීමට, කණ්ඩායම් ගබඩාවන්ට සහයෝගීකයින් වෙත තල්ලු කිරීමට සහ එකතු කිරීමට හැකිය.
teams.create_repo_permission_desc=මීට අමතරව, මෙම කණ්ඩායම ලබා දෙයි ගබඩාව සාදන්න අවසරය: සාමාජිකයින්ට සංවිධානයේ නව ගබඩාවක් නිර්මාණය කළ හැකිය.
teams.repositories=කණ්ඩායම් කෝෂ්ඨ
-teams.search_repo_placeholder=කෝෂ්ඨය සොයන්න…
teams.remove_all_repos_title=සියළුම කණ්ඩායම් කෝෂ්ඨ ඉවත් කරන්න
teams.remove_all_repos_desc=මෙය කණ්ඩායමෙන් සියළුම කෝෂ්ඨ ඉවත් කෙරෙනු ඇත.
teams.add_all_repos_title=සියළුම කෝෂ්ඨ එක්කරන්න
@@ -2026,6 +2015,8 @@ hooks=වෙබ්කොකු
authentication=සත්යාපන ප්රභවයන්
emails=පරිශීලක වි-තැපැල්
config=වින්යාසය
+config_summary=සාරාංශය
+config_settings=සැකසුම්
notices=පද්ධතියේ දැන්වීම්
monitor=අධීක්ෂණය
first_page=පළමු
@@ -2033,7 +2024,6 @@ last_page=පසුගිය
total=මුළු: %d
dashboard.statistic=සාරාංශය
-dashboard.operations=නඩත්තු මෙහෙයුම්
dashboard.system_status=පද්ධතියේ තත්වය
dashboard.operation_name=මෙහෙයුමේ නම
dashboard.operation_switch=මාරුවන්න
@@ -2173,9 +2163,6 @@ repos.unadopted.no_more=තවත් සම්මත නොකළ ගබඩා
repos.owner=හිමිකරු
repos.name=නම
repos.private=පෞද්ගලික
-repos.watches=අත් ඔරලෝසු
-repos.stars=තරු
-repos.forks=දෙබලක
repos.issues=ගැටළු
repos.size=ප්රමාණය
@@ -2273,7 +2260,6 @@ auths.tip.nextcloud=පහත සඳහන් මෙනුව භාවිතා
auths.tip.dropbox=https://www.dropbox.com/developers/apps හි නව යෙදුමක් සාදන්න
auths.tip.facebook=https://developers.facebook.com/apps හි නව යෙදුමක් ලියාපදිංචි කර නිෂ්පාදනය එකතු කරන්න “ෆේස්බුක් ලොගින් වන්න”
auths.tip.github=https://github.com/settings/applications/new හි නව OAUTH අයදුම්පතක් ලියාපදිංචි කරන්න
-auths.tip.gitlab=https://gitlab.com/profile/applications හි නව අයදුම්පතක් ලියාපදිංචි කරන්න
auths.tip.google_plus=ගූගල් API කොන්සෝලය වෙතින් OUT2 සේවාදායක අක්තපත්ර ලබා ගන්න https://console.developers.google.com/
auths.tip.openid_connect=අන්ත ලක්ෂ්ය නියම කිරීම සඳහා OpenID Connect ඩිස්කවරි URL (/.හොඳින් දැන /openid-වින්යාසය) භාවිතා කරන්න
auths.tip.twitter=https://dev.twitter.com/apps වෙත යන්න, යෙදුමක් සාදන්න සහ “මෙම යෙදුම ට්විටර් සමඟ පුරනය වීමට භාවිතා කිරීමට ඉඩ දෙන්න” විකල්පය සක්රීය කර ඇති බවට සහතික වන්න
@@ -2456,6 +2442,7 @@ notices.desc=සවිස්තරය
notices.op=ඔප්.
notices.delete_success=පද්ධති දැන්වීම් මකා දමා ඇත.
+
[action]
create_repo=නිර්මිත ගබඩාව %s
rename_repo=%[1]s
සිට %[3]sදක්වා නම් කරන ලද ගබඩාව
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index 1c3ca5ae43..b468b55283 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -140,6 +140,13 @@ confirm_delete_selected=Potvrdzujete zmazanie všetkých vybraných položiek?
name=Meno
value=Hodnota
+filter.is_archived=Archivované
+filter.is_template=Šablóna
+filter.private=Súkromný
+
+
+[search]
+
[aria]
navbar=Navigačná lišta
footer=Päta
@@ -309,7 +316,6 @@ collaborative_repos=Kolaboratívne repozitáre
my_orgs=Moje organizácie
my_mirrors=Moje zrkadlá
view_home=Zobraziť %s
-search_repos=Nájsť repozitár…
filter=Ostatné filtre
filter_by_team_repositories=Filtrovať podľa tímových repozitárov
feed_of=Informačný kanál „%s“
@@ -330,20 +336,8 @@ issues.in_your_repos=Vo vašich repozitároch
repos=Repozitáre
users=Používatelia
organizations=Organizácie
-search=Hľadať
go_to=Ísť na
code=Zdrojový kód
-search.type.tooltip=Typ vyhľadávania
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnúť iba výsledky, ktoré sa takmer zhodujú s hľadaným výrazom
-search.match=Zhoda
-search.match.tooltip=Zahrnúť iba výsledky, ktoré sa presne zhodujú s hľadaným výrazom
-code_search_unavailable=Vyhľadávanie kódu momentálne nie je dostupné. Kontaktujte, prosím, správcu.
-repo_no_results=Nenašli sa zodpovedajúce repozitáre.
-user_no_results=Nenašli sa zodpovedajúci používatelia.
-org_no_results=Nenašli sa zodpovedajúce organizácie.
-code_no_results=Nenašiel sa žiaden zdrojový kód zodpovedajúci hľadanému výrazu.
-code_search_results=`Výsledky hľadania pre "%s"`
code_last_indexed_at=Naposledy indexované %s
relevant_repositories_tooltip=Repozitáre, ktoré sú forkami alebo ktoré nemajú tému, žiadnu ikonu ani popis, sú skryté.
relevant_repositories=Zobrazujú sa iba relevantné repozitáre, zobraziť nefiltrované výsledky.
@@ -359,7 +353,6 @@ remember_me=Zapamätať si toto zariadenie
forgot_password_title=Zabudnuté heslo
forgot_password=Zabudli ste heslo?
sign_up_now=Potrebujete účet? Zaregistrujte sa teraz.
-confirmation_mail_sent_prompt=Na adresu %s bol odoslaný nový potvrdzovací e-mail. Skontrolujte si, prosím, vašu doručenú poštu počas najbližších %s pre dokončenie procesu registrácie.
allow_password_change=Vyžiadať od používateľa zmenu hesla (doporučuje sa)
reset_password_mail_sent_prompt=Na adresu %s bol odoslaný potvrdzovací e-mail. Skontrolujte si, prosím, vašu doručenú poštu počas najbližších %s pre dokončenie procesu obnovenia účtu.
active_your_account=Aktivovať účet
@@ -563,6 +556,7 @@ auth_failed=Overenie zlyhalo: %v
target_branch_not_exist=Cieľová vetva neexistuje.
+
[user]
change_avatar=Zmeniť svoj avatar…
joined_on=Pripojil/a sa %s
@@ -581,6 +575,7 @@ user_bio=Životopis
disabled_public_activity=Tento používateľ zákázal verejnú viditeľnosť aktivity.
+
[settings]
profile=Profil
account=Účet
@@ -706,7 +701,6 @@ gpg_invalid_token_signature=Zadaný GPG kľúč, podpis a token sa nezhodujú al
gpg_token_required=Musíte zadať podpis pre nižšie uvedený token
gpg_token=Token
gpg_token_help=Podpis môžete vygenerovať pomocou:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Zakódovaný (ASCII) podpis GPG
key_signature_gpg_placeholder=Začína s '-----BEGIN PGP SIGNATURE-----'
ssh_key_verified=Overený kľúč
@@ -852,7 +846,6 @@ visibility_helper_forced=Váš správca vynucuje že nové repozitáre musia by
visibility_fork_helper=(Zmena ovplyvní všetky forky.)
clone_helper=Potrebujete pomoc s klonovaním? Navštívte Pomocníka.
use_template=Použiť túto šablónu
-clone_in_vsc=Klonovať vo VS Code
generate_repo=Generovať repozitár
generate_from=Generovať z
repo_desc=Popis
@@ -1036,7 +1029,6 @@ editor.no_commit_to_branch=Nedá sa odoslať priamo do vetvy, pretože:
editor.require_signed_commit=Vetva vyžaduje podpísaný commit
commits.commits=Commity
-commits.find=Hľadať
commits.search_all=Všetky vetvy
commits.author=Autor
commits.message=Správa
@@ -1144,14 +1136,7 @@ activity.unresolved_conv_label=Otvoriť
activity.git_stats_commit_1=%d commit
activity.git_stats_commit_n=%d commity
-search=Hľadať
-search.type.tooltip=Typ vyhľadávania
-search.fuzzy=Fuzzy
-search.fuzzy.tooltip=Zahrnúť iba výsledky, ktoré sa takmer zhodujú s hľadaným výrazom
-search.match=Zhoda
-search.match.tooltip=Zahrnúť iba výsledky, ktoré sa presne zhodujú s hľadaným výrazom
-search.code_no_results=Nenašiel sa žiaden zdrojový kód zodpovedajúci hľadanému výrazu.
-search.code_search_unavailable=Vyhľadávanie kódu momentálne nie je dostupné. Kontaktujte, prosím, správcu.
+contributors.contribution_type.commits=Commitov
settings.collaboration.owner=Vlastník
settings.hooks=Webhooky
@@ -1246,6 +1231,8 @@ release.cancel=Zrušiť
+[graphs]
+
[org]
code=Kód
lower_repositories=repozitáre
@@ -1282,7 +1269,6 @@ dashboard.delete_generated_repository_avatars=Odstrániť vygenerované avatary
repos.owner=Vlastník
repos.private=Súkromný
-repos.forks=Forky
packages.owner=Vlastník
packages.repository=Repozitár
@@ -1328,6 +1314,7 @@ monitor.process.cancel=Zrušiť proces
+
[action]
compare_commits=Porovnať %d commitov
compare_commits_general=Porovnať commity
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 411a83ed75..9234e9aa58 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -91,6 +91,14 @@ concept_user_organization=Organisation
name=Namn
+filter.is_archived=Arkiverade
+filter.is_template=Mall
+filter.public=Offentlig
+filter.private=Privat
+
+
+[search]
+
[aria]
[heatmap]
@@ -212,7 +220,6 @@ collaborative_repos=Kollaborativa Utvecklingskataloger
my_orgs=Mina organisationer
my_mirrors=Mina speglar
view_home=Visa %s
-search_repos=Hitta en utvecklingskatalog…
filter=Övriga Filter
show_archived=Arkiverade
@@ -231,12 +238,7 @@ issues.in_your_repos=I dina utvecklingskataloger
repos=Utvecklingskataloger
users=Användare
organizations=Organisationer
-search=Sök
code=Kod
-repo_no_results=Inga matchande utvecklingskataloger hittades.
-user_no_results=Inga matchande användare hittades.
-org_no_results=Inga matchande organisationer hittades.
-code_no_results=Ingen källkod hittades som matchar din sökterm.
code_last_indexed_at=Indexerades senast %s
[auth]
@@ -249,7 +251,6 @@ remember_me=Kom ihåg denna enhet
forgot_password_title=Glömt lösenord
forgot_password=Glömt lösenord?
sign_up_now=Behöver du ett konto? Registrera nu.
-confirmation_mail_sent_prompt=Ett nytt bekräftelsemail has skickats till %s. Vänligen kolla din inkorg inom dom kommande %s för att slutföra registreringsprocessen.
must_change_password=Ändra ditt lösenord
allow_password_change=Kräv att användaren byter lösenord (rekommenderas)
reset_password_mail_sent_prompt=Ett nytt bekräftelsemail has skickats till %s. Vänligen kontrollera din inkorg inom de kommande %s för att slutföra återställning av ditt konto.
@@ -390,6 +391,7 @@ auth_failed=Autentisering misslyckades: %v
target_branch_not_exist=Målgrenen finns inte.
+
[user]
change_avatar=Byt din avatar…
repositories=Utvecklingskataloger
@@ -405,6 +407,7 @@ user_bio=Biografi
disabled_public_activity=Den här användaren har inaktiverat den publika synligheten av aktiviteten.
+
[settings]
profile=Profil
account=Konto
@@ -797,8 +800,6 @@ editor.require_signed_commit=Branchen kräver en signerad commit
commits.desc=Bläddra i källkodens förändringshistorik.
commits.commits=Incheckningar
-commits.search=Sök commits…
-commits.find=Sök
commits.search_all=Alla brancher
commits.author=Upphovsman
commits.message=Meddelande
@@ -826,7 +827,6 @@ projects.edit=Redigera projekt
projects.modify=Uppdatera projekt
projects.type.none=Ingen
projects.template.desc=Projektmall
-projects.type.uncategorized=Okatergoriserad
projects.column.edit_title=Namn
projects.column.new_title=Namn
projects.open=Öppna
@@ -1083,7 +1083,6 @@ pulls.compare_changes_desc=Välj branchen att merga in i, och ifrån.
pulls.compare_base=merga in i
pulls.compare_compare=pulla från
pulls.filter_branch=Filtrera gren
-pulls.no_results=Inga resultat hittades.
pulls.nothing_to_compare=Dessa brancher är ekvivalenta. Det finns ingen anledning att skapa en pull-request.
pulls.create=Skapa Pullförfrågan
pulls.title_desc=vill sammanfoga %[1]d incheckningar från s[2]s
in i %[3]s
@@ -1227,10 +1226,7 @@ activity.git_stats_and_deletions=och
activity.git_stats_deletion_1=%d borttagen
activity.git_stats_deletion_n=%d borttagningar
-search=Sök
-search.search_repo=Sök utvecklingskatalog
-search.results=Sökresultat för ”%s” i %s
-search.code_no_results=Ingen källkod hittades som matchar din sökterm.
+contributors.contribution_type.commits=Incheckningar
settings=Inställningar
settings.desc=Inställningarna är där du kan hantera inställningar för utvecklingskatalogen
@@ -1312,7 +1308,6 @@ settings.delete_collaborator=Ta bort
settings.collaborator_deletion=Ta bort medarbetare
settings.collaborator_deletion_desc=Borttagning av en medarbetare kommer att återkalla deras åtkomst till utvecklingskatalogen. Vill du fortsätta?
settings.remove_collaborator_success=Medarbetaren har blivit borttagen.
-settings.search_user_placeholder=Sök användare…
settings.org_not_allowed_to_be_collaborator=Organisationer kan inte läggas till som en medarbetare.
settings.change_team_access_not_allowed=Att ändra teamåtkomst för utvecklingskatalogen har begränsats till organisationsägaren
settings.team_not_in_organization=Teamet är inte i samma organisation som utvecklingskatalogen
@@ -1404,9 +1399,7 @@ settings.protect_enable_push=Aktivera Push
settings.protect_enable_push_desc=Alla med skrivrättigheter kommer att kunna pusha till denna branch (men inte force-pusha).
settings.protect_whitelist_deploy_keys=Vitlista deploy-nyckar med skrivåtkomst till push.
settings.protect_whitelist_users=Vitlistade användare för pushning:
-settings.protect_whitelist_search_users=Sök användare…
settings.protect_whitelist_teams=Vitlistade team för pushning:
-settings.protect_whitelist_search_teams=Sök team…
settings.protect_merge_whitelist_committers=Aktivera vitlista för sammanfogning
settings.protect_merge_whitelist_committers_desc=Tillåt endast vitlistade användare eller team att sammanfoga pull requests i denna branch.
settings.protect_merge_whitelist_users=Vitlistade användare för sammanfogning:
@@ -1535,6 +1528,8 @@ topic.count_prompt=Du kan inte välja fler än 25 ämnen
+[graphs]
+
[org]
org_name_holder=Organisationsnamn
org_full_name_holder=Organisationens Fullständiga Namn
@@ -1621,7 +1616,6 @@ teams.write_permission_desc=Medlemskap i detta team ger skrivrättighete
teams.admin_permission_desc=Medlemskap i detta team ger administratörsrättigheter: medlemmar kan läsa, pusha och lägga till medarbetare till teamets utvecklingskataloger.
teams.create_repo_permission_desc=Vidare så ger detta team Skapa utvecklingskatalog rättigheten: medlemmar can skapa nya utvecklingskataloger i organisationen.
teams.repositories=Teamförråd
-teams.search_repo_placeholder=Sök utvecklingskatalog…
teams.remove_all_repos_title=Ta bort alla utvecklingskataloger för teamet
teams.remove_all_repos_desc=Detta kommer att ta bort alla utvecklingskataloger från teamet.
teams.add_all_repos_title=Lägg till alla utvecklingskataloger
@@ -1644,6 +1638,8 @@ organizations=Organisationer
repositories=Utvecklingskataloger
authentication=Autentiseringskälla
config=Konfiguration
+config_summary=Översikt
+config_settings=Inställningar
notices=Systemaviseringar
monitor=Övervakning
first_page=Första
@@ -1651,7 +1647,6 @@ last_page=Sista
total=Totalt: %d
dashboard.statistic=Översikt
-dashboard.operations=Operationer för underhåll
dashboard.system_status=Status
dashboard.operation_name=Operationsnamn
dashboard.operation_switch=Byt till
@@ -1745,9 +1740,6 @@ repos.repo_manage_panel=Utvecklingskatalogshantering
repos.owner=Ägare
repos.name=Namn
repos.private=Privat
-repos.watches=Vakter
-repos.stars=Stjärnor
-repos.forks=Forkar
repos.issues=Ärenden
repos.size=Storlek
@@ -1813,7 +1805,6 @@ auths.tip.bitbucket=Registrera en ny OAuth konsument på https://bitbucket.org/a
auths.tip.dropbox=Skapa en ny applikation på https://www.dropbox.com/developers/apps
auths.tip.facebook=Registrera en ny appliaktion på https://developers.facebook.com/apps och lägg till produkten ”Facebook-inloggning”
auths.tip.github=Registrera en ny OAuth applikation på https://github.com/settings/applications/new
-auths.tip.gitlab=Registrera en ny applikation på https://gitlab.com/profile/applications
auths.tip.google_plus=Erhåll inloggningsuppgifter för OAuth2 från Google API-konsolen på https://console.developers.google.com/
auths.tip.openid_connect=Använd OpenID Connect Discovery länken (/.well-known/openid-configuration) för att ange slutpunkterna
auths.tip.twitter=Gå till https://dev.twitter.com/app, skapa en applikation och försäkra att alternativet "Allow this application to be used to Sign in with Twitter" är aktiverat
@@ -1971,6 +1962,7 @@ notices.desc=Beskrivning
notices.op=Op.
notices.delete_success=Systemnotifikationer har blivit raderade.
+
[action]
create_repo=skapade utvecklingskatalog %s
rename_repo=döpte om utvecklingskalatogen från %[1]s
till %[3]s
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index dd7d1b066e..119e1ef150 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -17,6 +17,7 @@ template=Şablon
language=Dil
notifications=Bildirimler
active_stopwatch=Etkin Zaman Takibi
+tracked_time_summary=Konu listesi süzgeçlerine dayanan takip edilen zamanın özeti
create_new=Oluştur…
user_profile_and_more=Profil ve Ayarlar…
signed_in_as=Giriş yapan:
@@ -90,6 +91,7 @@ remove=Kaldır
remove_all=Tümünü Kaldır
remove_label_str=`"%s" öğesini kaldır`
edit=Düzenle
+view=Görüntüle
enabled=Aktifleştirilmiş
disabled=Devre Dışı
@@ -97,6 +99,7 @@ locked=Kilitli
copy=Kopyala
copy_url=URL'yi kopyala
+copy_hash=Hash'i kopyala
copy_content=İçeriği kopyala
copy_branch=Dal adını kopyala
copy_success=Kopyalandı!
@@ -109,6 +112,7 @@ loading=Yükleniyor…
error=Hata
error404=Ulaşmaya çalıştığınız sayfa mevcut değil veya görüntüleme yetkiniz yok.
+go_back=Geri Git
never=Asla
unknown=Bilinmiyor
@@ -137,6 +141,15 @@ confirm_delete_selected=Tüm seçili öğeleri gerçekten silmek istiyor musunuz
name=İsim
value=Değer
+filter=Filtre
+filter.is_archived=Arşivlenmiş
+filter.is_template=Şablon
+filter.public=Genel
+filter.private=Özel
+
+
+[search]
+
[aria]
navbar=Gezinti Çubuğu
footer=Alt Bilgi
@@ -180,6 +193,7 @@ network_error=Ağ hatası
[startpage]
app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi
install=Kurulumu kolay
+install_desc=Platformunuz için ikili dosyayı çalıştırın, Docker ile yükleyin veya paket olarak edinin.
platform=Farklı platformlarda çalışablir
platform_desc=Gitea Go ile derleme yapılabilecek her yerde çalışmaktadır: Windows, macOS, Linux, ARM, vb. Hangisini seviyorsanız onu seçin!
lightweight=Hafif
@@ -309,7 +323,6 @@ collaborative_repos=Katkıya Açık Depolar
my_orgs=Organizasyonlarım
my_mirrors=Yansılarım
view_home=%s Görüntüle
-search_repos=Depo bul…
filter=Diğer Süzgeçler
filter_by_team_repositories=Takım depolarına göre süz
feed_of=`"%s" beslemesi`
@@ -330,20 +343,8 @@ issues.in_your_repos=Depolarınızda
repos=Depolar
users=Kullanıcılar
organizations=Organizasyonlar
-search=Ara
go_to=Git
code=Kod
-search.type.tooltip=Arama türü
-search.fuzzy=Bulanık
-search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer
-search.match=Eşleştir
-search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
-code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticinizle bağlantıya geçin.
-repo_no_results=Eşleşen depo bulunamadı.
-user_no_results=Eşleşen kullanıcı bulunamadı.
-org_no_results=Eşleşen organizasyon bulunamadı.
-code_no_results=Arama teriminizi içeren kaynak kod bulunamadı.
-code_search_results=`"%s" için sonuçları ara`
code_last_indexed_at=Son dizinlenen %s
relevant_repositories_tooltip=Çatal olan veya konusu, simgesi veya açıklaması olmayan depolar gizlenmiştir.
relevant_repositories=Sadece ilişkili depolar gösteriliyor, süzülmemiş sonuçları göster.
@@ -356,11 +357,11 @@ disable_register_prompt=Kayıt işlemi devre dışıdır. Lütfen site yönetici
disable_register_mail=Kayıt için e-posta doğrulama devre dışıdır.
manual_activation_only=Etkinleştirmeyi tamamlamak için site yöneticinizle bağlantıya geçin.
remember_me=Bu Aygıtı hatırla
+remember_me.compromised=Oturum açma tokeni artık geçerli değil, bu ele geçirilmiş bir hesaba işaret ediyor olabilir. Lütfen hesabınızda olağandışı faaliyet olup olmadığını denetleyin.
forgot_password_title=Şifremi unuttum
forgot_password=Şifrenizi mi unuttunuz?
sign_up_now=Bir hesaba mı ihtiyacınız var? Hemen kaydolun.
sign_up_successful=Hesap başarılı bir şekilde oluşturuldu. Hoşgeldiniz!
-confirmation_mail_sent_prompt=Yeni onay e-postası %s adresine gönderildi. Lütfen gelen kutunuzu bir sonraki %s e kadar kontrol edip kayıt işlemini tamamlayın.
must_change_password=Parolanızı güncelleyin
allow_password_change=Kullanıcıyı parola değiştirmeye zorla (önerilen)
reset_password_mail_sent_prompt=%s adresine bir onay e-postası gönderildi. Hesap kurtarma işlemini tamamlamak için lütfen gelen kutunuzu sonraki %s içinde kontrol edin.
@@ -375,6 +376,7 @@ email_not_associate=Bu e-posta adresi hiçbir hesap ile ilişkilendirilmemiştir
send_reset_mail=Hesap Kurtarma E-postası Gönder
reset_password=Hesap Kurtarma
invalid_code=Doğrulama kodunuz geçersiz veya süresi dolmuş.
+invalid_code_forgot_password=Onay kodunuz hatalı veya süresi geçmiş. Yeni bir oturum başlatmak için buraya tıklayın.
invalid_password=Parolanız hesap oluşturulurken kullanılan parolayla eşleşmiyor.
reset_password_helper=Hesabı Kurtar
reset_password_wrong_user=%s olarak oturum açmışsınız, ancak hesap kurtarma bağlantısı %s için
@@ -581,6 +583,7 @@ org_still_own_packages=Bu organizasyon hala bir veya daha fazla pakete sahip, ö
target_branch_not_exist=Hedef dal mevcut değil.
+
[user]
change_avatar=Profil resmini değiştir…
joined_on=%s tarihinde katıldı
@@ -606,6 +609,7 @@ form.name_reserved=`"%s" kullanıcı adı rezerve edilmiş.`
form.name_pattern_not_allowed=Kullanıcı adında "%s" deseni kullanılamaz.
form.name_chars_not_allowed=`"%s" kullanıcı adı geçersiz karakterler içeriyor.`
+
[settings]
profile=Profil
account=Hesap
@@ -676,6 +680,7 @@ choose_new_avatar=Yeni Avatar Seç
update_avatar=Profil Resmini Güncelle
delete_current_avatar=Güncel Avatarı Sil
uploaded_avatar_not_a_image=Yüklenen dosya bir resim dosyası değil.
+uploaded_avatar_is_too_big=Yüklenen dosyanın boyutu (%d KiB), azami boyutu (%d KiB) aşıyor.
update_avatar_success=Profil resminiz değiştirildi.
update_user_avatar_success=Kullanıcının avatarı güncellendi.
@@ -749,7 +754,6 @@ gpg_invalid_token_signature=Verilen GPG anahtarı, imza ve anahtar uyuşmuyor ve
gpg_token_required=Aşağıdaki anahtar için bir imza sağlamalısınız
gpg_token=Anahtar
gpg_token_help=Şunu kullanarak bir imza oluşturabilirsiniz:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Korumalı GPG imzası
key_signature_gpg_placeholder='-----PGP İMZA BAŞLAT -----' ile başlar
verify_gpg_key_success=GPG anahtarı "%s" doğrulandı.
@@ -857,6 +861,7 @@ revoke_oauth2_grant_description=Bu üçüncü taraf uygulamasına erişimin ipta
revoke_oauth2_grant_success=Erişim başarıyla kaldırıldı.
twofa_desc=İki faktörlü kimlik doğrulama, hesabınızın güvenliğini artırır.
+twofa_recovery_tip=Aygıtınızı kaybetmeniz durumunda, hesabınıza tekrar erişmek için tek kullanımlık kurtarma anahtarını kullanabileceksiniz.
twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmiş.
twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş.
twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak
@@ -879,6 +884,8 @@ webauthn_register_key=Güvenlik Anahtarı Ekle
webauthn_nickname=Takma Ad
webauthn_delete_key=Güvenlik Anahtarını Kaldır
webauthn_delete_key_desc=Bir güvenlik anahtarını kaldırırsanız, onunla artık giriş yapamazsınız. Devam edilsin mi?
+webauthn_key_loss_warning=Güvenlik anahtarlarınızı kaybederseniz, hesabınıza erişimi kaybedersiniz.
+webauthn_alternative_tip=Ek bir kimlik doğrulama yöntemi ayarlamak isteyebilirsiniz.
manage_account_links=Bağlı Hesapları Yönet
manage_account_links_desc=Bu harici hesaplar Gitea hesabınızla bağlantılı.
@@ -915,6 +922,7 @@ visibility.private=Özel
visibility.private_tooltip=Sadece katıldığınız organizasyonların üyeleri tarafından görünür
[repo]
+new_repo_helper=Bir depo, sürüm geçmişi dahil tüm proje dosyalarını içerir. Zaten başka bir yerde mi barındırıyorsunuz? Depoyu taşıyın.
owner=Sahibi
owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir.
repo_name=Depo İsmi
@@ -935,9 +943,10 @@ fork_from=Buradan Çatalla
already_forked=%s deposunu zaten çatalladınız
fork_to_different_account=Başka bir hesaba çatalla
fork_visibility_helper=Çatallanmış bir deponun görünürlüğü değiştirilemez.
+fork_branch=Çatala klonlanacak dal
+all_branches=Tüm dallar
fork_no_valid_owners=Geçerli bir sahibi olmadığı için bu depo çatallanamaz.
use_template=Bu şablonu kullan
-clone_in_vsc=VS Code'ta klonla
download_zip=ZIP indir
download_tar=TAR.GZ indir
download_bundle=BUNDLE indir
@@ -958,12 +967,13 @@ readme_helper=Bir README dosyası şablonu seçin.
readme_helper_desc=Projeniz için eksiksiz bir açıklama yazabileceğiniz yer burasıdır.
auto_init=Depoyu başlat (.gitignore, Lisans ve README dosyalarını ekler)
trust_model_helper=İmza doğrulaması için güven modelini seçin. Olası seçenekler şunlardır:
-trust_model_helper_collaborator=Ortak çalışan: Ortak çalışanların imzalarına güven
+trust_model_helper_collaborator=Katkıcı: Katkıcıların imzalarına güven
trust_model_helper_committer=İşleyen: İşleyenlerle eşleşen imzalara güven
-trust_model_helper_collaborator_committer=Ortak çalışan+İşleyen: İşleyenle eşleşen ortak çalışanların imzalarına güven
+trust_model_helper_collaborator_committer=Katkıcı+İşleyen: İşleyenle eşleşen ortak çalışanların imzalarına güven
trust_model_helper_default=Varsayılan: Bu kurulum için varsayılan güven modelini kullan
create_repo=Depo Oluştur
default_branch=Varsayılan Dal
+default_branch_label=varsayılan
default_branch_helper=Varsayılan dal, değişiklik istekleri ve kod işlemeleri için temel daldır.
mirror_prune=Buda
mirror_prune_desc=Kullanılmayan uzak depoları izleyen referansları kaldır
@@ -999,8 +1009,13 @@ delete_preexisting=Önceden var olan dosyaları sil
delete_preexisting_content=%s içindeki dosyaları sil
delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi
blame_prior=Bu değişiklikten önceki suçu görüntüle
+blame.ignore_revs=.git-blame-ignore-revs dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için buraya tıklayın.
+blame.ignore_revs.failed=.git-blame-ignore-revs dosyasındaki sürümler yok sayılamadı.
author_search_tooltip=En fazla 30 kullanıcı görüntüler
+tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil
+tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil
+tree_path_not_found_tag=%[1] yolu, %[2]s etiketinde mevcut değil
transfer.accept=Aktarımı Kabul Et
transfer.accept_desc=`"%s" tarafına aktar`
@@ -1250,9 +1265,7 @@ commits.desc=Kaynak kodu değişiklik geçmişine göz atın.
commits.commits=İşleme
commits.no_commits=Ortak bir işleme yok. "%s" ve "%s" tamamen farklı geçmişlere sahip.
commits.nothing_to_compare=Bu dallar eşit.
-commits.search=İşlemeleri ara…
commits.search.tooltip=Anahtar kelimeleri "author:", "committer:", "after:" veya "before:" ile kullanabilirsiniz, örneğin "revert author:Alice before:2019-01-13".
-commits.find=Ara
commits.search_all=Tüm Dallar
commits.author=Yazar
commits.message=Mesaj
@@ -1264,6 +1277,7 @@ commits.signed_by_untrusted_user=Güvenilmeyen kullanıcı tarafından imzaland
commits.signed_by_untrusted_user_unmatched=İşleyici ile eşleşmeyen güvenilmeyen kullanıcı tarafından imzalanmış
commits.gpg_key_id=GPG Anahtar Kimliği
commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi
+commits.view_path=Geçmişte bu noktayı görüntüle
commit.operations=İşlemler
commit.revert=Geri Al
@@ -1302,7 +1316,6 @@ projects.type.basic_kanban=Kanban Tabanı
projects.type.bug_triage=Hata Triyajı
projects.template.desc=Proje şablonu
projects.template.desc_helper=Başlamak için bir proje şablonu seçin
-projects.type.uncategorized=Kategorize edilmemiş
projects.column.edit=Sütun Düzenle
projects.column.edit_title=İsim
projects.column.new_title=İsim
@@ -1310,10 +1323,7 @@ projects.column.new_submit=Sütun Oluştur
projects.column.new=Yeni Sütun
projects.column.set_default=Varsayılanı Ayarla
projects.column.set_default_desc=Bu sütunu kategorize edilmemiş konular ve değişiklik istekleri için varsayılan olarak ayarlayın
-projects.column.unset_default=Varsayılanları Geri Al
-projects.column.unset_default_desc=Bu sütunu varsayılan olarak geri al
projects.column.delete=Sutün Sil
-projects.column.deletion_desc=Bir proje sütununun silinmesi, ilgili tüm konuları 'Kategorize edilmemiş'e taşır. Devam edilsin mi?
projects.column.color=Renk
projects.open=Aç
projects.close=Kapat
@@ -1425,7 +1435,6 @@ issues.filter_sort.moststars=En çok yıldızlılar
issues.filter_sort.feweststars=En az yıldızlılar
issues.filter_sort.mostforks=En çok çatallananlar
issues.filter_sort.fewestforks=En az çatallananlar
-issues.keyword_search_unavailable=Anahtar kelime ile arama şu an mevcut değil. Lütfen site yöneticisiyle iletişime geçin.
issues.action_open=Açık
issues.action_close=Kapat
issues.action_label=Etiket
@@ -1474,8 +1483,17 @@ issues.ref_closed_from=`bu konuyu kapat%[4]s konuyu yeniden aç%[4]s %[2]s`
issues.ref_from=`%[1]s'den`
issues.author=Yazar
+issues.author_helper=Bu kullanıcı yazardır.
issues.role.owner=Sahibi
+issues.role.owner_helper=Bu kullanıcı bu deponun sahibidir.
issues.role.member=Üye
+issues.role.member_helper=Bu kullanıcı bu deponun sahibi olan organizasyonun üyesidir.
+issues.role.collaborator=Katkıcı
+issues.role.collaborator_helper=Kullanıcı bu depoya işbirliği için davet edildi.
+issues.role.first_time_contributor=İlk defa katkıcı
+issues.role.first_time_contributor_helper=Bu, bu kullanıcının bu depoya ilk katkısı.
+issues.role.contributor=Katılımcı
+issues.role.contributor_helper=Bu kullanıcı bu depoya daha önce işleme gönderdi.
issues.re_request_review=İncelemeyi yeniden iste
issues.is_stale=Bu incelemeden bu yana bu istekte değişiklikler oldu
issues.remove_request_review=İnceleme isteğini kaldır
@@ -1491,6 +1509,8 @@ issues.label_description=Etiket açıklaması
issues.label_color=Etiket rengi
issues.label_exclusive=Özel
issues.label_archive=Etiketi Arşivle
+issues.label_archived_filter=Arşivlenmiş etiketleri göster
+issues.label_archive_tooltip=Arşivlenmiş etiketler, etiket araması yapılırken varsayılan olarak önerilerin dışında tutuluyor.
issues.label_exclusive_desc=Kapsam/öğe
etiketini, diğer kapsam/
etiketleriyle ayrışık olacak şekilde adlandırın.
issues.label_exclusive_warning=Çakışan kapsamlı etiketler, bir konu veya değişiklik isteği etiketleri düzenlenirken kaldırılacaktır.
issues.label_count=%d etiket
@@ -1666,7 +1686,6 @@ pulls.compare_compare=şuradan çek
pulls.switch_comparison_type=Karşılaştırma türünü değiştir
pulls.switch_head_and_base=Ana ve temeli değiştir
pulls.filter_branch=Dal filtrele
-pulls.no_results=Sonuç bulunamadı.
pulls.show_all_commits=Tüm işlemeleri göster
pulls.show_changes_since_your_last_review=Son incelemenizden sonraki değişiklikleri göster
pulls.showing_only_single_commit=Sadece %[1]s işlemesindeki değişiklikler gösteriliyor
@@ -1745,6 +1764,7 @@ pulls.rebase_conflict_summary=Hata Mesajı
pulls.unrelated_histories=Birleştirme Başarısız: Birleştirme başlığı ve tabanı ortak bir geçmişi paylaşmıyor. İpucu: Farklı bir strateji deneyin
pulls.merge_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, taban güncellendi. İpucu: Tekrar deneyin.
pulls.head_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, ana güncellendi. İpucu: Tekrar deneyin.
+pulls.has_merged=Başarısız: Değişiklik isteği birleştirildi, yeniden birleştiremez veya hedef dalı değiştiremezsiniz.
pulls.push_rejected=Birleştirme Başarısız Oldu: Gönderme reddedildi. Bu depo için Git İstemcilerini inceleyin.
pulls.push_rejected_summary=Tam Red Mesajı
pulls.push_rejected_no_message=Birleştirme başarısız oldu: Gönderme reddedildi, ancak uzak bir mesaj yoktu.
Bu depo için Git İstemcilerini inceleyin
@@ -1756,6 +1776,8 @@ pulls.status_checks_failure=Bazı kontroller başarısız oldu
pulls.status_checks_error=Bazı kontroller hatalar bildirdi
pulls.status_checks_requested=Gerekli
pulls.status_checks_details=Ayrıntılar
+pulls.status_checks_hide_all=Tüm denetlemeleri gizle
+pulls.status_checks_show_all=Tüm denetlemeleri göster
pulls.update_branch=Dalı birleştirmeyle güncelle
pulls.update_branch_rebase=Dalı yeniden yapılandırmayla güncelle
pulls.update_branch_success=Dal güncellemesi başarıyla gerçekleştirildi
@@ -1764,6 +1786,11 @@ pulls.outdated_with_base_branch=Bu dal, temel dal ile güncel değil
pulls.close=Değişiklik İsteğini Kapat
pulls.closed_at=`%[2]s değişiklik isteğini kapattı`
pulls.reopened_at=`%[2]s değişiklik isteğini yeniden açtı`
+pulls.cmd_instruction_hint=`Komut satırı talimatlarını görüntüleyin.`
+pulls.cmd_instruction_checkout_title=Çekme
+pulls.cmd_instruction_checkout_desc=Proje deponuzdan yeni bir dalı çekin ve değişiklikleri test edin.
+pulls.cmd_instruction_merge_title=Birleştir
+pulls.cmd_instruction_merge_desc=Değişiklikleri birleştirin ve Gitea'da güncelleyin.
pulls.clear_merge_message=Birleştirme iletilerini temizle
pulls.clear_merge_message_hint=Birleştirme iletisini temizlemek sadece işleme ileti içeriğini kaldırır ama üretilmiş "Co-Authored-By …" gibi git fragmanlarını korur.
@@ -1809,6 +1836,8 @@ milestones.edit_success=`"%s" dönüm noktası güncellendi.`
milestones.deletion=Kilometre Taşını Sil
milestones.deletion_desc=Bir kilometre taşını silmek, onu ilgili tüm sorunlardan kaldırır. Devam edilsin mi?
milestones.deletion_success=Kilometre taşı silindi.
+milestones.filter_sort.earliest_due_data=En erken bitiş tarihi
+milestones.filter_sort.latest_due_date=En uzak bitiş tarihi
milestones.filter_sort.least_complete=En az tamamlama
milestones.filter_sort.most_complete=En çok tamamlama
milestones.filter_sort.most_issues=En çok konu
@@ -1924,16 +1953,7 @@ activity.git_stats_and_deletions=ve
activity.git_stats_deletion_1=%d silme oldu
activity.git_stats_deletion_n=%d silme oldu
-search=Ara
-search.search_repo=Depo ara
-search.type.tooltip=Arama türü
-search.fuzzy=Belirsiz
-search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer
-search.match=Eşleştir
-search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer
-search.results=`"%s" için %s içinde sonuçları ara`
-search.code_no_results=Arama teriminizi içeren kaynak kod bulunamadı.
-search.code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticinizle bağlantıya geçin.
+contributors.contribution_type.commits=İşleme
settings=Ayarlar
settings.desc=Ayarlar, deponun ayarlarını yönetebileceğiniz yerdir
@@ -1971,6 +1991,8 @@ settings.mirror_settings.push_mirror.add=Yansı Gönderimi Ekle
settings.mirror_settings.push_mirror.edit_sync_time=Yansı eşzamanlama aralığını düzenle
settings.sync_mirror=Şimdi Eşitle
+settings.pull_mirror_sync_in_progress=Şu an %s uzak sunucusundan değişiklikler çekiliyor.
+settings.push_mirror_sync_in_progress=Şu an %s uzak sunucusuna değişiklikler itiliyor.
settings.site=Web Sitesi
settings.update_settings=Ayarları Güncelle
settings.update_mirror_settings=Yansı Ayarları Güncelle
@@ -2010,6 +2032,7 @@ settings.pulls.default_allow_edits_from_maintainers=Bakımcıların düzenlemele
settings.releases_desc=Depo Sürümlerini Etkinleştir
settings.packages_desc=Depo Paket Kütüğünü Etkinleştir
settings.projects_desc=Depo Projelerini Etkinleştir
+settings.projects_mode_all=Tüm projeler
settings.actions_desc=Depo İşlemlerini Etkinleştir
settings.admin_settings=Yönetici Ayarları
settings.admin_enable_health_check=Depo Sağlık Kontrollerini Etkinleştir (git fsck)
@@ -2084,7 +2107,6 @@ settings.delete_collaborator=Sil
settings.collaborator_deletion=Katkıcıyı Sil
settings.collaborator_deletion_desc=Bir katkıcıyı silmek, bu depoya erişimini iptal edecektir. Devam et?
settings.remove_collaborator_success=Katkıcı silindi.
-settings.search_user_placeholder=Kullanıcı ara…
settings.org_not_allowed_to_be_collaborator=Organizasyonlar katkıcı olarak eklenemez.
settings.change_team_access_not_allowed=Depo için takım erişimini değiştirmek, organizasyon sahibiyle sınırlandırıldı
settings.team_not_in_organization=Takım, depo ile aynı organizasyonda değil
@@ -2092,7 +2114,6 @@ settings.teams=Takımlar
settings.add_team=Takım Ekle
settings.add_team_duplicate=Takım zaten bu depoya sahip
settings.add_team_success=Takım artık bu depoya erişebilir.
-settings.search_team=Takım Ara…
settings.change_team_permission_tip=Takımın izni takım ayarı sayfasında ayarlanır ve depo başına değiştirilemez
settings.delete_team_tip=Bu takımın tüm depolara erişimi var ve kaldırılamıyor
settings.remove_team_success=Takımın depoya erişimi kaldırıldı.
@@ -2104,12 +2125,14 @@ settings.webhook_deletion_desc=Bir web isteğini kaldırmak, ayarlarını ve tes
settings.webhook_deletion_success=Web isteği silindi.
settings.webhook.test_delivery=Test Dağıtımı
settings.webhook.test_delivery_desc=Bu web isteğini sahte bir olayla test edin.
+settings.webhook.test_delivery_desc_disabled=Bu web istemcisini sahte bir olayla denemek için etkinleştirin.
settings.webhook.request=İstekler
settings.webhook.response=Cevaplar
settings.webhook.headers=Başlıklar
settings.webhook.payload=İçerik
settings.webhook.body=Gövde
settings.webhook.replay.description=Bu web kancasını tekrar çalıştır.
+settings.webhook.replay.description_disabled=Bu web istemcisini yeniden oynatmak için etkinleştirin.
settings.webhook.delivery.success=Teslim kuyruğuna bir olay eklendi. Teslim geçmişinde görünmesi birkaç saniye alabilir.
settings.githooks_desc=Git İstemcileri Git'in kendisi tarafından desteklenmektedir. Özel işlemler ayarlamak için aşağıdaki istemci dosyalarını düzenleyebilirsiniz.
settings.githook_edit_desc=İstek aktif değilse örnek içerik sunulacaktır. İçeriği boş bırakmak, isteği devre dışı bırakmayı beraberinde getirecektir.
@@ -2243,9 +2266,7 @@ settings.protect_whitelist_committers=Beyaz Liste Kısıtlı Gönderme
settings.protect_whitelist_committers_desc=Sadece beyaz listeye alınmış kullanıcıların veya takımların bu dala göndermesine izin verilir (ancak zorla gönderim yapmayın).
settings.protect_whitelist_deploy_keys=Beyaz liste göndermek için yazma erişimi olan anahtarları dağıtır.
settings.protect_whitelist_users=İtme için beyaz listedeki kullanıcılar:
-settings.protect_whitelist_search_users=Kullanıcı ara…
settings.protect_whitelist_teams=İtme için beyaz listedeki takımlar:
-settings.protect_whitelist_search_teams=Takımları ara…
settings.protect_merge_whitelist_committers=Birleştirme Beyaz Listesini Etkinleştir
settings.protect_merge_whitelist_committers_desc=Yalnızca beyaz listedeki kullanıcıların veya takımların bu daldaki değişiklik isteklerini birleştirmesine izin verin.
settings.protect_merge_whitelist_users=Birleştirme için beyaz listedeki kullanıcılar:
@@ -2269,6 +2290,7 @@ settings.dismiss_stale_approvals_desc=Değişiklik isteğinin içeriğini deği
settings.require_signed_commits=İmzalı İşleme Gerekli
settings.require_signed_commits_desc=Reddetme, onlar imzasızsa veya doğrulanamazsa bu dala gönderir.
settings.protect_branch_name_pattern=Korunmuş Dal Adı Deseni
+settings.protect_branch_name_pattern_desc=Korunmuş dal isim desenleri. Desen sözdizimi için belgelere bakabilirsiniz. Örnekler: main, release/**
settings.protect_patterns=Desenler
settings.protect_protected_file_patterns=Korumalı dosya kalıpları (noktalı virgülle ayrılmış ';'):
settings.protect_protected_file_patterns_desc=Kullanıcının bu dalda dosya ekleme, düzenleme veya silme hakları olsa bile doğrudan değiştirilmesine izin verilmeyen korumalı dosyalar. Birden çok desen noktalı virgül (';') kullanılarak ayrılabilir. Desen sözdizimi için github.com/gobwas/glob belgelerine bakın. Örnekler: .drone.yml
, /docs/**/*.txt
.
@@ -2305,6 +2327,7 @@ settings.tags.protection.allowed.teams=İzin verilen takımlar
settings.tags.protection.allowed.noone=Hiç kimse
settings.tags.protection.create=Etiketi Koru
settings.tags.protection.none=Korumalı etiket yok.
+settings.tags.protection.pattern.description=Birden çok etiketi eşleştirmek için tek bir ad, glob deseni veya normal ifade kullanabilirsiniz. Daha fazlası için korumalı etiketler rehberini okuyun.
settings.bot_token=Bot Jetonu
settings.chat_id=Sohbet Kimliği
settings.thread_id=İş Parçacığı ID
@@ -2516,6 +2539,8 @@ error.csv.too_large=Bu dosya çok büyük olduğu için işlenemiyor.
error.csv.unexpected=%d satırı ve %d sütununda beklenmeyen bir karakter içerdiğinden bu dosya işlenemiyor.
error.csv.invalid_field_count=%d satırında yanlış sayıda alan olduğundan bu dosya işlenemiyor.
+[graphs]
+
[org]
org_name_holder=Organizasyon Adı
org_full_name_holder=Organizasyon Tam Adı
@@ -2620,7 +2645,6 @@ teams.write_permission_desc=Bu takım Yazma erişimi veriyor.
teams.admin_permission_desc=Bu takım Yönetici erişimi veriyor. Üyeler takım depolarını okuyabilir, itebilir ve katkıcı ekleyebilir.
teams.create_repo_permission_desc=Ayrıca, bu takım Depo oluşturma izni verir: üyeler organizasyonda yeni depolar oluşturabilir.
teams.repositories=Takım Depoları
-teams.search_repo_placeholder=Depo ara…
teams.remove_all_repos_title=Tüm takım depolarını kaldır
teams.remove_all_repos_desc=Bu, tüm depoları takımdan kaldıracaktır.
teams.add_all_repos_title=Tüm depoları ekle
@@ -2652,6 +2676,8 @@ integrations=Bütünleştirmeler
authentication=Yetkilendirme Kaynakları
emails=Kullanıcı E-postaları
config=Yapılandırma
+config_summary=Özet
+config_settings=Ayarlar
notices=Sistem Bildirimler
monitor=İzleme
first_page=İlk
@@ -2661,7 +2687,6 @@ settings=Yönetici Ayarları
dashboard.new_version_hint=Gitea %s şimdi hazır, %s çalıştırıyorsunuz. Ayrıntılar için blog'a bakabilirsiniz.
dashboard.statistic=Özet
-dashboard.operations=Bakım İşlemleri
dashboard.system_status=Sistem Durumu
dashboard.operation_name=İşlem Adı
dashboard.operation_switch=Geç
@@ -2701,6 +2726,7 @@ dashboard.reinit_missing_repos=Kayıtları bulunanlar için tüm eksik Git depol
dashboard.sync_external_users=Harici kullanıcı verisini senkronize et
dashboard.cleanup_hook_task_table=Hook_task tablosunu temizleme
dashboard.cleanup_packages=Süresi dolmuş paketleri temizleme
+dashboard.cleanup_actions=Eylemlerin süresi geçmiş günlük ve yapılarını temizle
dashboard.server_uptime=Sunucunun Ayakta Kalma Süresi
dashboard.current_goroutine=Güncel Goroutine'ler
dashboard.current_memory_usage=Güncel Bellek Kullanımı
@@ -2738,7 +2764,9 @@ dashboard.gc_lfs=LFS üst nesnelerin atıklarını temizle
dashboard.stop_zombie_tasks=Zombi görevleri durdur
dashboard.stop_endless_tasks=Daimi görevleri durdur
dashboard.cancel_abandoned_jobs=Terkedilmiş görevleri iptal et
+dashboard.start_schedule_tasks=Zamanlanmış görevleri başlat
dashboard.sync_branch.started=Dal Eşzamanlaması başladı
+dashboard.rebuild_issue_indexer=Konu indeksini yeniden oluştur
users.user_manage_panel=Kullanıcı Hesap Yönetimi
users.new_account=Yeni Kullanıcı Hesabı
@@ -2747,6 +2775,9 @@ users.full_name=Tam İsim
users.activated=Aktifleştirilmiş
users.admin=Yönetici
users.restricted=Kısıtlanmış
+users.reserved=Rezerve
+users.bot=Bot
+users.remote=Uzak
users.2fa=2FD
users.repos=Depolar
users.created=Oluşturuldu
@@ -2793,6 +2824,7 @@ users.list_status_filter.is_prohibit_login=Oturum Açmayı Önle
users.list_status_filter.not_prohibit_login=Oturum Açmaya İzin Ver
users.list_status_filter.is_2fa_enabled=2FA Etkin
users.list_status_filter.not_2fa_enabled=2FA Devre Dışı
+users.details=Kullanıcı Ayrıntıları
emails.email_manage_panel=Kullanıcı E-posta Yönetimi
emails.primary=Birincil
@@ -2805,6 +2837,7 @@ emails.updated=E-posta güncellendi
emails.not_updated=İstenen e-posta adresi güncellenemedi: %v
emails.duplicate_active=Bu e-posta adresi farklı bir kullanıcı için zaten aktif.
emails.change_email_header=E-posta Özelliklerini Güncelle
+emails.change_email_text=Bu e-posta adresini güncellemek istediğinizden emin misiniz?
orgs.org_manage_panel=Organizasyon Yönetimi
orgs.name=İsim
@@ -2818,9 +2851,6 @@ repos.unadopted.no_more=Kabul edilmemiş başka depo bulunamadı
repos.owner=Sahibi
repos.name=İsim
repos.private=Özel
-repos.watches=İzlemeler
-repos.stars=Yıldızlar
-repos.forks=Çatallar
repos.issues=Konular
repos.size=Boyut
repos.lfs_size=LFS Boyutu
@@ -2829,6 +2859,7 @@ packages.package_manage_panel=Paket Yönetimi
packages.total_size=Toplam Boyut: %s
packages.unreferenced_size=Referanssız Boyut: %s
packages.cleanup=Süresi dolmuş veriyi temizle
+packages.cleanup.success=Süresi dolmuş veri başarıyla temizlendi
packages.owner=Sahibi
packages.creator=Oluşturan
packages.name=İsim
@@ -2839,10 +2870,12 @@ packages.size=Boyut
packages.published=Yayınlandı
defaulthooks=Varsayılan Web İstemcileri
+defaulthooks.desc=Web İstemcileri, belirli Gitea olayları tetiklendiğinde otomatik olarak HTTP POST isteklerini sunucuya yapar. Burada tanımlanan Web İstemcileri varsayılandır ve tüm yeni depolara kopyalanır. web istemcileri kılavuzunda daha fazla bilgi edinin.
defaulthooks.add_webhook=Varsayılan Web İstemcisi Ekle
defaulthooks.update_webhook=Varsayılan Web İstemcisini Güncelle
systemhooks=Sistem Web İstemcileri
+systemhooks.desc=Belirli Gitea olayları tetiklendiğinde Web istemcileri otomatik olarak bir sunucuya HTTP POST istekleri yapar. Burada tanımlanan web istemcileri sistemdeki tüm depolar üzerinde çalışır, bu yüzden lütfen bunun olabilecek tüm performans sonuçlarını göz önünde bulundurun. web istemcileri kılavuzunda daha fazla bilgi edinin.
systemhooks.add_webhook=Sistem Web İstemcisi Ekle
systemhooks.update_webhook=Sistem Web İstemcisi Güncelle
@@ -2942,11 +2975,11 @@ auths.tip.nextcloud=Aşağıdaki "Ayarlar -> Güvenlik -> OAuth 2.0 istemcisi" m
auths.tip.dropbox=https://www.dropbox.com/developers/apps adresinde yeni bir uygulama oluştur
auths.tip.facebook=https://developers.facebook.com/apps adresinde yeni bir uygulama kaydedin ve "Facebook Giriş" ürününü ekleyin
auths.tip.github=https://github.com/settings/applications/new adresinde yeni bir OAuth uygulaması kaydedin
-auths.tip.gitlab=https://gitlab.com/profile/applications adresinde yeni bir uygulama kaydedin
auths.tip.google_plus=OAuth2 istemci kimlik bilgilerini https://console.developers.google.com/ adresindeki Google API konsolundan edinin
auths.tip.openid_connect=Bitiş noktalarını belirlemek için OpenID Connect Discovery URL'sini kullanın (/.well-known/openid-configuration)
auths.tip.twitter=https://dev.twitter.com/apps adresine gidin, bir uygulama oluşturun ve “Bu uygulamanın Twitter ile oturum açmak için kullanılmasına izin ver” seçeneğinin etkin olduğundan emin olun
auths.tip.discord=https://discordapp.com/developers/applications/me adresinde yeni bir uygulama kaydedin
+auths.tip.gitea=Yeni bir OAuth2 uygulaması kaydedin. Rehber https://docs.gitea.com/development/oauth2-provider adresinde bulunabilir
auths.tip.yandex=`https://oauth.yandex.com/client/new adresinde yeni bir uygulama oluşturun. "Yandex.Passport API'sı" bölümünden aşağıdaki izinleri seçin: "E-posta adresine erişim", "Kullanıcı avatarına erişim" ve "Kullanıcı adına, ad ve soyadına, cinsiyete erişim"`
auths.tip.mastodon=Kimlik doğrulaması yapmak istediğiniz mastodon örneği için özel bir örnek URL girin (veya varsayılan olanı kullanın)
auths.edit=Kimlik Doğrulama Kaynağı Düzenle
@@ -3126,8 +3159,10 @@ monitor.queue.name=İsim
monitor.queue.type=Tür
monitor.queue.exemplar=Örnek Türü
monitor.queue.numberworkers=Çalışan Sayısı
+monitor.queue.activeworkers=Etkin Çalışanlar
monitor.queue.maxnumberworkers=En Fazla Çalışan Sayısı
monitor.queue.numberinqueue=Kuyruktaki Sayı
+monitor.queue.review_add=Çalışanları İncele / Ekle
monitor.queue.settings.title=Havuz Ayarları
monitor.queue.settings.desc=Havuzlar, çalışan kuyruğu tıkanmasına bir yanıt olarak dinamik olarak büyürler.
monitor.queue.settings.maxnumberworkers=En fazla çalışan Sayısı
@@ -3153,6 +3188,7 @@ notices.desc=Açıklama
notices.op=İşlem
notices.delete_success=Sistem bildirimleri silindi.
+
[action]
create_repo=depo %s oluşturuldu
rename_repo=%[1]s
olan depo adını %[3]s buna çevirdi
@@ -3337,6 +3373,8 @@ rpm.registry=Bu kütüğü komut satırını kullanarak kurun:
rpm.distros.redhat=RedHat tabanlı dağıtımlarda
rpm.distros.suse=SUSE tabanlı dağıtımlarda
rpm.install=Paketi kurmak için, aşağıdaki komutu çalıştırın:
+rpm.repository=Depo Bilgisi
+rpm.repository.architectures=Mimariler
rubygems.install=Paketi gem ile kurmak için, şu komutu çalıştırın:
rubygems.install2=veya paketi Gemfile dosyasına ekleyin:
rubygems.dependencies.runtime=Çalışma Zamanı Bağımlılıkları
@@ -3454,23 +3492,29 @@ runners.status.idle=Boşta
runners.status.active=Etkin
runners.status.offline=Çevrimdışı
runners.version=Sürüm
+runners.reset_registration_token=Kayıt tokenini sıfırla
runners.reset_registration_token_success=Çalıştırıcı kayıt belirteci başarıyla sıfırlandı
runs.all_workflows=Tüm İş Akışları
runs.commit=İşle
+runs.scheduled=Zamanlanmış
runs.pushed_by=iten
runs.invalid_workflow_helper=İş akışı yapılandırma dosyası geçersiz. Lütfen yapılandırma dosyanızı denetleyin: %s
+runs.no_matching_online_runner_helper=Şu etiket ile eşleşen çevrimiçi çalıştırıcı bulunamadı: %s
runs.actor=Aktör
runs.status=Durum
runs.actors_no_select=Tüm aktörler
runs.status_no_select=Tüm durumlar
runs.no_results=Eşleşen sonuç yok.
+runs.no_workflows=Henüz hiç bir iş akışı yok.
runs.no_runs=İş akışı henüz hiç çalıştırılmadı.
+runs.empty_commit_message=(boş işleme iletisi)
workflow.disable=İş Akışını Devre Dışı Bırak
workflow.disable_success='%s' iş akışı başarıyla devre dışı bırakıldı.
workflow.enable=İş Akışını Etkinleştir
workflow.enable_success='%s' iş akışı başarıyla etkinleştirildi.
+workflow.disabled=İş akışı devre dışı.
need_approval_desc=Değişiklik isteği çatalında iş akışı çalıştırmak için onay gerekiyor.
@@ -3481,7 +3525,6 @@ variables.none=Henüz hiçbir değişken yok.
variables.deletion=Değişkeni kaldır
variables.deletion.description=Bir değişkeni kaldırma kalıcıdır ve geri alınamaz. Devam edilsin mi?
variables.description=Değişkenler belirli işlemlere aktarılacaktır, bunun dışında okunamaz.
-variables.id_not_exist=%d kimlikli değişken mevcut değil.
variables.edit=Değişkeni Düzenle
variables.deletion.failed=Değişken kaldırılamadı.
variables.deletion.success=Değişken kaldırıldı.
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 4cd6c44571..e8a3acedda 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -101,6 +101,15 @@ concept_user_organization=Організація
name=Назва
+filter=Фільтр
+filter.is_archived=Архівовані
+filter.is_template=Шаблон
+filter.public=Публічний
+filter.private=Приватний
+
+
+[search]
+
[aria]
[heatmap]
@@ -236,7 +245,6 @@ collaborative_repos=Спільні репозиторії
my_orgs=Мої організації
my_mirrors=Мої дзеркала
view_home=Переглянути %s
-search_repos=Шукати репозиторій…
filter=Інші фільтри
filter_by_team_repositories=Фільтрувати за репозиторіями команд
feed_of=`Стрічка "%s"`
@@ -257,14 +265,7 @@ issues.in_your_repos=В ваших репозиторіях
repos=Репозиторії
users=Користувачі
organizations=Організації
-search=Пошук
code=Код
-search.fuzzy=Неточний
-search.match=Відповідність
-repo_no_results=Відповідних репозиторіїв не знайдено.
-user_no_results=Відповідних користувачів не знайдено.
-org_no_results=Відповідних організацій не знайдено.
-code_no_results=Відповідний пошуковому запитанню код не знайдено.
code_last_indexed_at=Останні індексовані %s
[auth]
@@ -277,7 +278,6 @@ remember_me=Запам’ятати цей пристрій
forgot_password_title=Забув пароль
forgot_password=Забули пароль?
sign_up_now=Потрібен обліковий запис? Зареєструйтеся зараз.
-confirmation_mail_sent_prompt=Новий лист для підтвердження було відправлено на %s, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації.
must_change_password=Оновіть свій пароль
allow_password_change=Вимагати в користувача змінити пароль (рекомендується)
reset_password_mail_sent_prompt=Електронний лист із підтвердженням надіслано %s. Перевірте папку 'Вхідні' в межах наступних %s, щоб завершити процес відновлення облікового запису.
@@ -466,6 +466,7 @@ auth_failed=Помилка автентифікації: %v
target_branch_not_exist=Цільової гілки не існує.
+
[user]
change_avatar=Змінити свій аватар…
repositories=Репозиторії
@@ -482,6 +483,7 @@ user_bio=Біографія
disabled_public_activity=Цей користувач вимкнув публічний показ діяльності.
+
[settings]
profile=Профіль
account=Обліковий запис
@@ -598,7 +600,6 @@ gpg_invalid_token_signature=Наданий ключ GPG, підпис і ток
gpg_token_required=Вам потрібно надати підпис для нижчевказаного токена
gpg_token=Токен
gpg_token_help=Ви можете створити підпис за допомогою:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Текстовий (armored) підпис GPG
key_signature_gpg_placeholder=`Починається з "-----BEGIN PGP SIGNATURE-----"`
ssh_key_verified=Перевірений ключ
@@ -737,7 +738,6 @@ fork_repo=Форкнути репозиторій
fork_from=Форк з
fork_visibility_helper=Неможливо змінити видимість форкнутого репозиторію.
use_template=Застосувати цей шаблон
-clone_in_vsc=Клонувати у VS Code
download_zip=Завантажити ZIP
download_tar=Завантажити TAR.GZ
download_bundle=Завантажити BUNDLE
@@ -979,8 +979,6 @@ editor.require_signed_commit=Гілка вимагає підписаного к
commits.desc=Переглянути історію зміни коду.
commits.commits=Коміти
commits.nothing_to_compare=Ці гілки однакові.
-commits.search=Знайти коміт…
-commits.find=Пошук
commits.search_all=Усі гілки
commits.author=Автор
commits.message=Повідомлення
@@ -1018,7 +1016,6 @@ projects.type.basic_kanban=Спрощений канбан
projects.type.bug_triage=Сортування помилок
projects.template.desc=Шаблон проєкту
projects.template.desc_helper=Оберіть шаблон проєкту, аби почати
-projects.type.uncategorized=Без категорії
projects.column.edit_title=Назва
projects.column.new_title=Назва
projects.column.color=Колір
@@ -1310,7 +1307,6 @@ pulls.compare_compare=pull з
pulls.switch_comparison_type=Перемкнути вигляд порівняння
pulls.switch_head_and_base=Поміняти місцями основну та базову гілку
pulls.filter_branch=Фільтр по гілці
-pulls.no_results=Результатів не знайдено.
pulls.nothing_to_compare=Ці гілки однакові. Немає необхідності створювати запитів на злиття.
pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім.
pulls.has_pull_request=`Запит злиття для цих гілок вже існує: %[2]s#%[3]d`
@@ -1508,12 +1504,7 @@ activity.git_stats_and_deletions=та
activity.git_stats_deletion_1=%d видалений
activity.git_stats_deletion_n=%d видалені
-search=Пошук
-search.search_repo=Пошук репозиторію
-search.fuzzy=Неточний
-search.match=Збігається
-search.results=Результати пошуку для "%s" в %s
-search.code_no_results=Відповідний пошуковому запитанню код не знайдено.
+contributors.contribution_type.commits=Коміти
settings=Налаштування
settings.desc=У налаштуваннях ви можете змінювати різні параметри цього репозиторія
@@ -1630,7 +1621,6 @@ settings.delete_collaborator=Видалити
settings.collaborator_deletion=Видалити співавтора
settings.collaborator_deletion_desc=Цей користувач більше не матиме доступу для спільної роботи в цьому репозиторії після видалення. Ви хочете продовжити?
settings.remove_collaborator_success=Співавтор видалений.
-settings.search_user_placeholder=Пошук користувача…
settings.org_not_allowed_to_be_collaborator=Організації не можуть бути додані як співавтори.
settings.change_team_access_not_allowed=Зміна доступу команди до репозитарію обмежена власником організації
settings.team_not_in_organization=Команда та репозитарій мають привязки до різних організацій
@@ -1638,7 +1628,6 @@ settings.teams=Команди
settings.add_team=Додати Команду
settings.add_team_duplicate=Команда вже має привязку до репозитарію
settings.add_team_success=Команда отримала доступ до репозиторію.
-settings.search_team=Знайти команду…
settings.change_team_permission_tip=Дозволи команди встановлюються на сторінці налаштувань команди та не можуть бути заданими для кожного з репозиторіїв окремо
settings.delete_team_tip=Ця команда має доступ до всіх репозиторіїв та не може бути видалена
settings.remove_team_success=Доступ команди до репозиторію видалений.
@@ -1755,9 +1744,7 @@ settings.protect_whitelist_committers=Білий список обмеження
settings.protect_whitelist_committers_desc=Лише користувачі та команди з білого списку зможуть виконувати push в цій гілці (за виключеням force push).
settings.protect_whitelist_deploy_keys=Білий список ключів розгортання з правом на запис.
settings.protect_whitelist_users=Користувачі, які можуть робити push в цю гілку:
-settings.protect_whitelist_search_users=Пошук користувачів…
settings.protect_whitelist_teams=Команди, учасники яких можуть робити push в цю гілку:
-settings.protect_whitelist_search_teams=Пошук команд…
settings.protect_merge_whitelist_committers=Обмежити право на прийняття Pull Request'ів в цю гілку списком
settings.protect_merge_whitelist_committers_desc=Ви можете додавати користувачів або цілі команди в 'білий' список цієї гілки. Тільки присутні в списку зможуть приймати запити на злиття. В іншому випадку будь-хто з правами запису до головного репозиторію буде володіти такою можливістю.
settings.protect_merge_whitelist_users=Користувачі з правом на прийняття Pull Request'ів в цю гілку:
@@ -1961,6 +1948,8 @@ error.csv.too_large=Не вдається відобразити цей файл
error.csv.unexpected=Не вдається відобразити цей файл, тому що він містить неочікуваний символ в рядку %d і стовпці %d.
error.csv.invalid_field_count=Не вдається відобразити цей файл, тому що він має неправильну кількість полів у рядку %d.
+[graphs]
+
[org]
org_name_holder=Назва організації
org_full_name_holder=Повна назва організації
@@ -2052,7 +2041,6 @@ teams.write_permission_desc=Ця команда надає доступ на адміністраторський доступ: учасники можуть читати, виконувати push команди та додавати співробітників до репозиторію.
teams.create_repo_permission_desc=Крім того, ця команда надає дозвіл Створити репозиторій: учасники можуть створювати нові репозиторії в організації.
teams.repositories=Репозиторії команди
-teams.search_repo_placeholder=Пошук репозиторію…
teams.remove_all_repos_title=Видалити всі репозиторії команди
teams.remove_all_repos_desc=Це видалить усі репозиторії команди.
teams.add_all_repos_title=Додати всі репозиторії
@@ -2077,6 +2065,8 @@ hooks=Веб-хуки
authentication=Джерела автентифікації
emails=Електронні адреси Користувача
config=Конфігурація
+config_summary=Підсумок
+config_settings=Налаштування
notices=Сповіщення системи
monitor=Моніторинг
first_page=Перша
@@ -2084,7 +2074,6 @@ last_page=Остання
total=Разом: %d
dashboard.statistic=Підсумок
-dashboard.operations=Технічне обслуговування
dashboard.system_status=Статус системи
dashboard.operation_name=Назва операції
dashboard.operation_switch=Перемкнути
@@ -2225,9 +2214,6 @@ repos.unadopted.no_more=Не знайдено більше неприйняти
repos.owner=Власник
repos.name=Назва
repos.private=Приватний
-repos.watches=Стежать
-repos.stars=В обраному
-repos.forks=Форки
repos.issues=Задачі
repos.size=Розмір
@@ -2325,7 +2311,6 @@ auths.tip.nextcloud=`Зареєструйте нового споживача OA
auths.tip.dropbox=Додайте новий додаток на https://www.dropbox.com/developers/apps
auths.tip.facebook=`Створіть новий додаток на https://developers.facebook.com/apps і додайте модуль "Facebook Login"`
auths.tip.github=Додайте OAuth додаток на https://github.com/settings/applications/new
-auths.tip.gitlab=Додайте новий додаток на https://gitlab.com/profile/applications
auths.tip.google_plus=Отримайте облікові дані клієнта OAuth2 в консолі Google API на сторінці https://console.developers.google.com/
auths.tip.openid_connect=Використовуйте OpenID Connect Discovery URL (/.well-known/openid-configuration) для автоматичної настройки входу OAuth
auths.tip.twitter=Перейдіть на https://dev.twitter.com/apps, створіть програму і переконайтеся, що включена опція «Дозволити цю програму для входу в систему за допомогою Twitter»
@@ -2510,6 +2495,7 @@ notices.desc=Опис
notices.op=Оп.
notices.delete_success=Сповіщення системи були видалені.
+
[action]
create_repo=створив(ла) репозиторій %s
rename_repo=репозиторій перейменовано з %[1]s
на %[3]s
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 6d22468c9d..3e907eabfd 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -25,6 +25,7 @@ enable_javascript=此网站需要 JavaScript。
toc=目录
licenses=许可证
return_to_gitea=返回 Gitea
+more_items=更多选项
username=用户名
email=电子邮件地址
@@ -113,6 +114,7 @@ loading=正在加载...
error=错误
error404=您正尝试访问的页面 不存在 或 您尚未被授权 查看该页面。
go_back=返回
+invalid_data=无效数据: %v
never=从不
unknown=未知
@@ -123,6 +125,7 @@ pin=固定
unpin=取消置顶
artifacts=制品
+confirm_delete_artifact=您确定要删除制品'%s'吗?
archived=已归档
@@ -141,6 +144,38 @@ confirm_delete_selected=确认删除所有选中项目?
name=名称
value=值
+filter=过滤
+filter.clear=清除筛选器
+filter.is_archived=已归档
+filter.not_archived=非存档
+filter.is_fork=派生
+filter.not_fork=非派生
+filter.is_mirror=镜像
+filter.not_mirror=非镜像
+filter.is_template=模板
+filter.not_template=非模板
+filter.public=公开
+filter.private=私有库
+
+no_results_found=未找到结果
+
+[search]
+search=搜索...
+type_tooltip=搜索类型
+fuzzy=模糊
+match=匹配
+repo_kind=搜索仓库...
+user_kind=搜索用户...
+org_kind=搜索组织...
+team_kind=搜索团队...
+code_kind=搜索代码...
+package_kind=搜索软件包...
+project_kind=搜索项目...
+branch_kind=搜索分支...
+commit_kind=搜索提交记录...
+runner_kind=搜索runners...
+no_results=未找到匹配结果
+
[aria]
navbar=导航栏
footer=页脚
@@ -305,6 +340,7 @@ env_config_keys=环境配置
env_config_keys_prompt=以下环境变量也将应用于您的配置文件:
[home]
+nav_menu=导航菜单
uname_holder=用户名或邮箱
password_holder=密码
switch_dashboard_context=切换控制面板用户
@@ -314,7 +350,6 @@ collaborative_repos=参与协作的仓库
my_orgs=我的组织
my_mirrors=我的镜像
view_home=访问 %s
-search_repos=查找仓库…
filter=其他过滤器
filter_by_team_repositories=按团队仓库筛选
feed_of=`"%s"的源`
@@ -335,20 +370,8 @@ issues.in_your_repos=在您的仓库中
repos=仓库
users=用户
organizations=组织
-search=搜索
go_to=转到
code=代码
-search.type.tooltip=搜索类型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似匹配搜索词的结果
-search.match=匹配
-search.match.tooltip=仅包含精确匹配搜索词的结果
-code_search_unavailable=目前代码搜索不可用。请与网站管理员联系。
-repo_no_results=未找到匹配的仓库。
-user_no_results=未找到匹配的用户。
-org_no_results=未找到匹配的组织。
-code_no_results=未找到与搜索字词匹配的源代码。
-code_search_results=“%s” 的搜索结果是
code_last_indexed_at=最后索引于 %s
relevant_repositories_tooltip=派生的仓库,以及缺少主题、图标和描述的仓库将被隐藏。
relevant_repositories=只显示相关的仓库, 显示未过滤结果。
@@ -366,7 +389,6 @@ forgot_password_title=忘记密码
forgot_password=忘记密码?
sign_up_now=还没帐户?马上注册。
sign_up_successful=帐户创建成功。欢迎!
-confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 %s,请检查您的收件箱并在 %s 内完成确认注册操作。
must_change_password=更新您的密码
allow_password_change=要求用户更改密码(推荐)
reset_password_mail_sent_prompt=确认电子邮件已被发送到 %s。请您在 %s 内检查您的收件箱 ,完成密码重置过程。
@@ -423,6 +445,7 @@ authorization_failed_desc=因为检测到无效请求,授权失败。请尝试
sspi_auth_failed=SSPI 认证失败
password_pwned=此密码出现在 被盗密码 列表上并且曾经被公开。 请使用另一个密码再试一次。
password_pwned_err=无法完成对 HaveIBeenPwned 的请求
+last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
[mail]
view_it_on=在 %s 上查看
@@ -588,6 +611,8 @@ org_still_own_packages=该组织仍然是一个或多个软件包的拥有者,
target_branch_not_exist=目标分支不存在。
+admin_cannot_delete_self=当您是管理员时,您不能删除自己。请先移除您的管理员权限
+
[user]
change_avatar=修改头像
joined_on=加入于 %s
@@ -613,6 +638,14 @@ form.name_reserved=用户名 "%s" 被保留。
form.name_pattern_not_allowed=用户名中不允许使用 "%s" 格式。
form.name_chars_not_allowed=用户名 "%s" 包含无效字符。
+block.block=屏蔽
+block.block.user=屏蔽用户
+block.block.org=屏蔽用户访问组织
+block.block.failure=屏蔽用户失败: %s
+block.unblock=取消屏蔽
+block.title=屏蔽一个用户
+block.info_2=关注你的账号
+
[settings]
profile=个人信息
account=账号
@@ -757,7 +790,6 @@ gpg_invalid_token_signature=提供的 GPG 密钥、签名和令牌不匹配或
gpg_token_required=您必须为下面的令牌提供签名
gpg_token=令牌
gpg_token_help=您可以使用以下方式生成签名:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=GPG 增强签名
key_signature_gpg_placeholder=以 '-----BEGIN PGP PUBLIC KEY BLOCK-----' 开头
verify_gpg_key_success=GPG 密钥 %s 已被验证。
@@ -951,7 +983,6 @@ fork_branch=要克隆到 Fork 的分支
all_branches=所有分支
fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的所有者。
use_template=使用此模板
-clone_in_vsc=在 VS Code 中克隆
download_zip=下载 ZIP
download_tar=下载 TAR.GZ
download_bundle=下载 BUNDLE
@@ -967,6 +998,8 @@ issue_labels_helper=选择一个工单标签集
license=授权许可
license_helper=选择授权许可文件。
license_helper_desc=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合你的项目?见 选择一个许可证
+object_format=对象格式
+object_format_helper=仓库的对象格式。之后无法更改。SHA1 是最兼容的。
readme=自述
readme_helper=选择自述文件模板。
readme_helper_desc=这是您可以为您的项目撰写完整描述的地方。
@@ -984,6 +1017,7 @@ mirror_prune=修剪
mirror_prune_desc=删除过时的远程跟踪引用
mirror_interval=镜像间隔 (有效的时间单位是 'h', 'm', 's')。0 禁用自动定期同步 (最短间隔: %s)
mirror_interval_invalid=镜像间隔无效。
+mirror_sync=已同步
mirror_sync_on_commit=推送提交时同步
mirror_address=从 URL 克隆
mirror_address_desc=在授权框中输入必要的凭据。
@@ -1034,6 +1068,7 @@ desc.public=公开
desc.template=模板
desc.internal=内部
desc.archived=已存档
+desc.sha256=SHA256
template.items=模板选项
template.git_content=Git数据(默认分支)
@@ -1184,6 +1219,8 @@ audio_not_supported_in_browser=您的浏览器不支持使用 HTML5 'video' 标
stored_lfs=存储到Git LFS
symbolic_link=符号链接
executable_file=可执行文件
+vendored=被供应的
+generated=已生成的
commit_graph=提交图
commit_graph.select=选择分支
commit_graph.hide_pr_refs=隐藏合并请求
@@ -1270,9 +1307,8 @@ commits.desc=浏览代码修改历史
commits.commits=次代码提交
commits.no_commits=没有共同的提交。%s 和 %s 的历史完全不同。
commits.nothing_to_compare=这些分支是相同的。
-commits.search=搜索提交历史
commits.search.tooltip=`您可以在关键词前加上前缀,如"author:", "committer:", "after:", 或"before:", 例如 "retrin author:Alice before:2019-01-13"`
-commits.find=搜索
+commits.search_branch=此分支
commits.search_all=所有分支
commits.author=作者
commits.message=备注
@@ -1323,7 +1359,6 @@ projects.type.basic_kanban=基础看板
projects.type.bug_triage=Bug分类看板
projects.template.desc=项目模板
projects.template.desc_helper=选择一个项目模板以开始
-projects.type.uncategorized=未分类
projects.column.edit=编辑列
projects.column.edit_title=名称
projects.column.new_title=名称
@@ -1331,8 +1366,6 @@ projects.column.new_submit=创建列
projects.column.new=创建列
projects.column.set_default=设为默认
projects.column.set_default_desc=设置此列为未分类问题和合并请求的默认值
-projects.column.unset_default=取消设为默认
-projects.column.unset_default_desc=取消此列为默认值
projects.column.delete=删除列
projects.column.deletion_desc=删除项目列会将所有相关问题移到“未分类”。是否继续?
projects.column.color=彩色
@@ -1446,7 +1479,6 @@ issues.filter_sort.moststars=点赞由多到少
issues.filter_sort.feweststars=点赞由少到多
issues.filter_sort.mostforks=派生由多到少
issues.filter_sort.fewestforks=派生由少到多
-issues.keyword_search_unavailable=关键词搜索目前不可用。请联系网站管理员。
issues.action_open=开启
issues.action_close=关闭
issues.action_label=标签
@@ -1698,7 +1730,6 @@ pulls.compare_compare=拉取从
pulls.switch_comparison_type=切换比较类型
pulls.switch_head_and_base=切换 head 和 base
pulls.filter_branch=过滤分支
-pulls.no_results=未找到结果
pulls.show_all_commits=显示所有提交
pulls.show_changes_since_your_last_review=显示自您上次审核以来的更改
pulls.showing_only_single_commit=仅显示提交 %[1]s 的更改
@@ -1707,6 +1738,7 @@ pulls.select_commit_hold_shift_for_range=选择提交。按住 Shift + 单击选
pulls.review_only_possible_for_full_diff=只有在查看全部差异时才能进行审核
pulls.filter_changes_by_commit=按提交筛选
pulls.nothing_to_compare=分支内容相同,无需创建合并请求。
+pulls.nothing_to_compare_have_tag=所选分支/标签相同。
pulls.nothing_to_compare_and_allow_empty_pr=这些分支是相等的,此合并请求将为空。
pulls.has_pull_request=这些分支之间的合并请求已存在: %[2]s#%[3]d
pulls.create=创建合并请求
@@ -1901,6 +1933,9 @@ wiki.page_name_desc=输入此 Wiki 页面的名称。特殊名称有:'Home', '
wiki.original_git_entry_tooltip=查看原始的 Git 文件而不是使用友好链接。
activity=动态
+activity.navbar.code_frequency=代码频率
+activity.navbar.contributors=贡献者
+activity.navbar.recent_commits=最近的提交
activity.period.filter_label=周期:
activity.period.daily=1 天
activity.period.halfweekly=3 天
@@ -1966,16 +2001,10 @@ activity.git_stats_and_deletions=和
activity.git_stats_deletion_1=删除 %d 行
activity.git_stats_deletion_n=删除 %d 行
-search=搜索
-search.search_repo=搜索仓库...
-search.type.tooltip=搜索类型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似匹配搜索词的结果
-search.match=匹配
-search.match.tooltip=仅包含精确匹配搜索词的结果
-search.results=在 %[3]s 中搜索 "%[1]s" 的结果
-search.code_no_results=未找到与搜索字词匹配的源代码。
-search.code_search_unavailable=当前代码搜索不可用。请与网站管理员联系。
+contributors.contribution_type.filter_label=贡献类型:
+contributors.contribution_type.commits=提交
+contributors.contribution_type.additions=更多
+contributors.contribution_type.deletions=删除
settings=设置
settings.desc=设置是你可以管理仓库设置的地方
@@ -2003,6 +2032,7 @@ settings.mirror_settings.docs.doc_link_title=如何镜像仓库?
settings.mirror_settings.docs.doc_link_pull_section=文档中的 “从远程仓库拉取” 部分。
settings.mirror_settings.docs.pulling_remote_title=从远程仓库拉取代码
settings.mirror_settings.mirrored_repository=镜像库
+settings.mirror_settings.pushed_repository=推送仓库
settings.mirror_settings.direction=方向
settings.mirror_settings.direction.pull=拉取
settings.mirror_settings.direction.push=推送
@@ -2024,6 +2054,8 @@ settings.branches.add_new_rule=添加新规则
settings.advanced_settings=高级设置
settings.wiki_desc=启用仓库百科
settings.use_internal_wiki=使用内置百科
+settings.default_wiki_branch_name=默认百科分支名称
+settings.failed_to_change_default_wiki_branch=更改百科默认分支失败。
settings.use_external_wiki=使用外部百科
settings.external_wiki_url=外部 Wiki 链接
settings.external_wiki_url_error=外部百科链接无效
@@ -2054,6 +2086,10 @@ settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者
settings.releases_desc=启用发布
settings.packages_desc=启用仓库软件包注册中心
settings.projects_desc=启用仓库项目
+settings.projects_mode_desc=项目模式 (要显示的项目类型)
+settings.projects_mode_repo=仅仓库项目
+settings.projects_mode_owner=仅限用户或组织项目
+settings.projects_mode_all=所有项目
settings.actions_desc=启用 Actions
settings.admin_settings=管理员设置
settings.admin_enable_health_check=启用仓库健康检查 (git fsck)
@@ -2079,6 +2115,7 @@ settings.convert_fork_succeed=此派生仓库已经转换为普通仓库。
settings.transfer=转移仓库所有权
settings.transfer.rejected=代码库转移被拒绝。
settings.transfer.success=代码库转移成功。
+settings.transfer.blocked_user=无法传输仓库,因为您被新的所有者屏蔽。
settings.transfer_abort=取消转移
settings.transfer_abort_invalid=你不能取消不存在的代码库转移。
settings.transfer_abort_success=成功取消了将代码库转让给 %s。
@@ -2101,7 +2138,7 @@ settings.trust_model.collaborator.long=协作者:信任协作者的签名
settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。
settings.trust_model.committer=提交者
settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub,这会强制采用 Gitea 作为提交者和签名者)
-settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须撇撇数据库种的一名用户。
+settings.trust_model.committer.desc=有效签名只有和提交者相匹配才会被标记为“受信任”,否则它们将被标记为“不匹配”。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须匹配数据库中的一名用户。
settings.trust_model.collaboratorcommitter=协作者+提交者
settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名
settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。
@@ -2124,11 +2161,11 @@ settings.add_collaborator_success=协作者添加成功!
settings.add_collaborator_inactive_user=无法添加未激活的用户作为合作者。
settings.add_collaborator_owner=不能将所有者添加为协作者。
settings.add_collaborator_duplicate=合作者已经被添加到本仓库。
+settings.add_collaborator.blocked_user=此写作者被仓库所有者屏蔽,反之亦然。
settings.delete_collaborator=删除
settings.collaborator_deletion=删除协作者
settings.collaborator_deletion_desc=删除协作者后他将无法再对此仓库的访问。继续?
settings.remove_collaborator_success=协作者删除成功!
-settings.search_user_placeholder=搜索用户...
settings.org_not_allowed_to_be_collaborator=组织不允许被添加为仓库协作者!
settings.change_team_access_not_allowed=更改仓库的团队访问权限仅限于组织所有者
settings.team_not_in_organization=团队不在与仓库相同的组织中
@@ -2136,7 +2173,6 @@ settings.teams=团队
settings.add_team=添加团队
settings.add_team_duplicate=团队已经拥有仓库
settings.add_team_success=团队现在可以访问仓库。
-settings.search_team=搜索团队...
settings.change_team_permission_tip=团队权限设置于团队设置页面,不能根据仓库更改
settings.delete_team_tip=该团队仍有仓库, 无法删除
settings.remove_team_success=团队访问仓库的权限已被删除。
@@ -2289,9 +2325,7 @@ settings.protect_whitelist_committers=受白名单限制的推送
settings.protect_whitelist_committers_desc=只有列入白名单的用户或团队才能被允许推送到此分支(但不能强行推送)。
settings.protect_whitelist_deploy_keys=具有推送权限的部署密钥白名单。
settings.protect_whitelist_users=推送白名单用户:
-settings.protect_whitelist_search_users=搜索用户...
settings.protect_whitelist_teams=推送白名单团队:
-settings.protect_whitelist_search_teams=搜索团队...
settings.protect_merge_whitelist_committers=启用合并白名单
settings.protect_merge_whitelist_committers_desc=仅允许白名单用户或团队合并合并请求到此分支。
settings.protect_merge_whitelist_users=合并白名单用户:
@@ -2312,6 +2346,8 @@ settings.protect_approvals_whitelist_users=审查者白名单:
settings.protect_approvals_whitelist_teams=审查团队白名单:
settings.dismiss_stale_approvals=取消过时的批准
settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。
+settings.ignore_stale_approvals=忽略过期批准
+settings.ignore_stale_approvals_desc=对旧提交(过期审核)的批准将不计入 PR 的批准数。如果过期审查已被驳回,则与此无关。
settings.require_signed_commits=需要签名提交
settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支
settings.protect_branch_name_pattern=受保护的分支名称模式
@@ -2367,6 +2403,7 @@ settings.archive.error=仓库在归档时出现异常。请通过日志获取详
settings.archive.error_ismirror=请不要对镜像仓库归档,谢谢!
settings.archive.branchsettings_unavailable=已归档仓库无法进行分支设置。
settings.archive.tagsettings_unavailable=已归档仓库的Git标签设置不可用。
+settings.archive.mirrors_unavailable=如果仓库已被归档,镜像将不可用。
settings.unarchive.button=撤销仓库归档
settings.unarchive.header=撤销此仓库归档
settings.unarchive.text=撤销归档将恢复仓库接收提交、推送,以及新工单和合并请求的能力。
@@ -2533,7 +2570,6 @@ branch.default_deletion_failed=不能删除默认分支"%s"。
branch.restore=`还原分支 "%s"`
branch.download=`下载分支 "%s"`
branch.rename=`重命名分支 "%s"`
-branch.search=搜索分支
branch.included_desc=此分支是默认分支的一部分
branch.included=已包含
branch.create_new_branch=从下列分支创建分支:
@@ -2564,6 +2600,16 @@ find_file.no_matching=没有找到匹配的文件
error.csv.too_large=无法渲染此文件,因为它太大了。
error.csv.unexpected=无法渲染此文件,因为它包含了意外字符,其位于第 %d 行和第 %d 列。
error.csv.invalid_field_count=无法渲染此文件,因为它在第 %d 行中的字段数有误。
+error.broken_git_hook=此仓库的 Git 钩子似乎已损坏。 请按照 文档 来修复它们,然后推送一些提交来刷新状态。
+
+[graphs]
+component_loading=正在加载 %s...
+component_loading_failed=无法加载 %s
+component_loading_info=这可能需要一点…
+component_failed_to_load=意外的错误发生了。
+code_frequency.what=代码频率
+contributors.what=贡献
+recent_commits.what=最近的提交
[org]
org_name_holder=组织名称
@@ -2669,7 +2715,6 @@ teams.write_permission_desc=该团队拥有对所属仓库的 读取管理 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。
teams.create_repo_permission_desc=此外,该团队拥有了 创建仓库 的权限:成员可以在组织中创建新的仓库。
teams.repositories=团队仓库
-teams.search_repo_placeholder=搜索仓库...
teams.remove_all_repos_title=移除所有团队仓库
teams.remove_all_repos_desc=这将从团队中移除所有仓库。
teams.add_all_repos_title=添加所有仓库
@@ -2678,6 +2723,7 @@ teams.add_nonexistent_repo=您尝试添加的仓库不存在,请先创建它
teams.add_duplicate_users=用户已经是团队成员。
teams.repos.none=此团队无法访问任何仓库。
teams.members.none=团队中没有成员。
+teams.members.blocked_user=不能添加用户因为他已经被该组织屏蔽。
teams.specific_repositories=指定仓库
teams.specific_repositories_helper=团队成员将只能访问添加到团队的仓库。 选择此项 将不会 自动删除已经添加的仓库。
teams.all_repositories=所有仓库
@@ -2690,7 +2736,9 @@ teams.invite.by=邀请人 %s
teams.invite.description=请点击下面的按钮加入团队。
[admin]
+maintenance=维护
dashboard=管理面板
+self_check=自我检查
identity_access=身份及认证
users=帐户管理
organizations=组织管理
@@ -2701,6 +2749,8 @@ integrations=集成
authentication=认证源
emails=用户邮件
config=应用配置
+config_summary=摘要
+config_settings=组织设置
notices=系统提示
monitor=监控面板
first_page=首页
@@ -2710,7 +2760,7 @@ settings=管理设置
dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 博客 了解详情。
dashboard.statistic=摘要
-dashboard.operations=维护操作
+dashboard.maintenance_operations=运维
dashboard.system_status=系统状态
dashboard.operation_name=操作名称
dashboard.operation_switch=开关
@@ -2736,6 +2786,7 @@ dashboard.delete_missing_repos=删除所有丢失 Git 文件的仓库
dashboard.delete_missing_repos.started=删除所有丢失 Git 文件的仓库任务已启动。
dashboard.delete_generated_repository_avatars=删除生成的仓库头像
dashboard.sync_repo_branches=将缺少的分支从 git 数据同步到数据库
+dashboard.sync_repo_tags=从 git 数据同步标签到数据库
dashboard.update_mirrors=更新镜像仓库
dashboard.repo_health_check=健康检查所有仓库
dashboard.check_repo_stats=检查所有仓库统计
@@ -2790,6 +2841,7 @@ dashboard.stop_endless_tasks=停止永不停止的任务
dashboard.cancel_abandoned_jobs=取消丢弃的任务
dashboard.start_schedule_tasks=开始调度任务
dashboard.sync_branch.started=分支同步已开始
+dashboard.sync_tag.started=标签同步已开始
dashboard.rebuild_issue_indexer=重建工单索引
users.user_manage_panel=用户帐户管理
@@ -2875,9 +2927,6 @@ repos.unadopted.no_more=找不到更多未被收录的仓库
repos.owner=所有者
repos.name=名称
repos.private=私有库
-repos.watches=关注数
-repos.stars=点赞数
-repos.forks=派生数
repos.issues=工单数
repos.size=大小
repos.lfs_size=LFS 大小
@@ -3002,7 +3051,7 @@ auths.tip.nextcloud=使用下面的菜单“设置(Settings) -> 安全(Sec
auths.tip.dropbox=在 https://www.dropbox.com/developers/apps 上创建一个新的应用程序
auths.tip.facebook=`在 https://developers.facebook.com/apps 注册一个新的应用,并添加产品"Facebook 登录"`
auths.tip.github=在 https://github.com/settings/applications/new 注册一个 OAuth 应用程序
-auths.tip.gitlab=在 https://gitlab.com/profile/applications 上注册新应用程序
+auths.tip.gitlab_new=在 https://gitlab.com/-/profile/applications 注册一个新的应用
auths.tip.google_plus=从谷歌 API 控制台 (https://console.developers.google.com/) 获得 OAuth2 客户端凭据
auths.tip.openid_connect=使用 OpenID 连接发现 URL (/.well-known/openid-configuration) 来指定终点
auths.tip.twitter=访问 https://dev.twitter.com/apps,创建应用并确保启用了"允许此应用程序用于登录 Twitter"的选项。
@@ -3138,6 +3187,7 @@ config.picture_config=图片和头像配置
config.picture_service=图片服务
config.disable_gravatar=禁用 Gravatar 头像
config.enable_federated_avatar=启用 Federated Avatars
+config.open_with_editor_app_help=用于克隆菜单的编辑器。如果为空将使用默认值。展开可以查看默认值。
config.git_config=Git 配置
config.git_disable_diff_highlight=禁用差异对比语法高亮
@@ -3216,6 +3266,14 @@ notices.desc=提示描述
notices.op=操作
notices.delete_success=系统通知已被删除。
+self_check.no_problem_found=尚未发现问题。
+self_check.startup_warnings=启动警告:
+self_check.database_collation_mismatch=期望数据库使用的校验方式:%s
+self_check.database_collation_case_insensitive=数据库正在使用一个校验 %s, 这是一个不敏感的校验. 虽然Gitea可以与它合作,但可能有一些罕见的情况不如预期的那样起作用。
+self_check.database_inconsistent_collation_columns=数据库正在使用%s的排序规则,但是这些列使用了不匹配的排序规则。这可能会造成一些意外问题。
+self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。
+self_check.database_fix_mssql=对于MSSQL用户,您现在只能通过"ALTER ... COLLATE ..."SQLs手动解决这个问题。
+
[action]
create_repo=创建了仓库 %s
rename_repo=重命名仓库 %[1]s
为 %[3]s
@@ -3400,6 +3458,9 @@ rpm.registry=从命令行设置此注册中心:
rpm.distros.redhat=在基于 RedHat 的发行版
rpm.distros.suse=在基于 SUSE 的发行版
rpm.install=要安装包,请运行以下命令:
+rpm.repository=仓库信息
+rpm.repository.architectures=架构
+rpm.repository.multiple_groups=此软件包可在多个组中使用。
rubygems.install=要使用 gem 安装软件包,请运行以下命令:
rubygems.install2=或将它添加到 Gemfile:
rubygems.dependencies.runtime=运行时依赖
@@ -3526,14 +3587,15 @@ runs.scheduled=已计划的
runs.pushed_by=推送者
runs.invalid_workflow_helper=工作流配置文件无效。请检查您的配置文件: %s
runs.no_matching_online_runner_helper=没有匹配标签的在线 runner: %s
+runs.no_job_without_needs=工作流必须包含至少一个没有依赖关系的作业。
runs.actor=操作者
runs.status=状态
runs.actors_no_select=所有操作者
runs.status_no_select=所有状态
runs.no_results=没有匹配的结果。
runs.no_workflows=目前还没有工作流。
-runs.no_workflows.quick_start=不知道如何启动Gitea Action?请参阅 快速启动指南
-runs.no_workflows.documentation=更多有关 Gitea Action 的信息,请访问 文档。
+runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗?请查看 快速启动指南。
+runs.no_workflows.documentation=关于Gitea Actions的更多信息,请参阅 文档。
runs.no_runs=工作流尚未运行过。
runs.empty_commit_message=(空白的提交消息)
@@ -3552,7 +3614,7 @@ variables.none=目前还没有变量。
variables.deletion=删除变量
variables.deletion.description=删除变量是永久性的,无法撤消。继续吗?
variables.description=变量将被传给特定的 Actions,其它情况将不能读取
-variables.id_not_exist=ID %d 变量不存在。
+variables.id_not_exist=ID为 %d 的变量不存在。
variables.edit=编辑变量
variables.deletion.failed=删除变量失败。
variables.deletion.success=变量已被删除。
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index d4074026fd..d4b65239a6 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -61,6 +61,12 @@ concept_code_repository=儲存庫
name=組織名稱
+filter.is_template=樣板
+filter.private=私有庫
+
+
+[search]
+
[aria]
[heatmap]
@@ -116,13 +122,11 @@ issues.in_your_repos=屬於該用戶儲存庫的
repos=儲存庫
users=使用者
organizations=組織
-search=搜尋
[auth]
register_helper_msg=已經註冊?立即登錄!
forgot_password_title=忘記密碼
forgot_password=忘記密碼?
-confirmation_mail_sent_prompt=一封新的確認郵件已發送至 %s。請檢查您的收件箱並在 %s 小時內完成確認註冊操作。
active_your_account=啟用您的帳戶
has_unconfirmed_mail=%s 您好,您有一封發送至( %s) 但未被確認的郵件。如果您未收到啟用郵件,或需要重新發送,請單擊下方的按鈕。
resend_mail=單擊此處重新發送確認郵件
@@ -195,6 +199,7 @@ auth_failed=授權驗證失敗:%v
target_branch_not_exist=目標分支不存在
+
[user]
repositories=儲存庫列表
activity=公開活動
@@ -204,6 +209,7 @@ follow=關注
unfollow=取消關注
+
[settings]
profile=個人訊息
password=修改密碼
@@ -374,7 +380,6 @@ editor.cancel=取消
editor.no_changes_to_show=沒有可以顯示的變更。
commits.commits=次程式碼提交
-commits.find=搜尋
commits.author=作者
commits.message=備註
commits.date=提交日期
@@ -480,7 +485,6 @@ issues.dependency.remove=移除成員
pulls.new=建立合併請求
pulls.compare_changes=建立合併請求
pulls.filter_branch=過濾分支
-pulls.no_results=未找到結果
pulls.create=建立合併請求
pulls.merged_title_desc=於 %[4]s 將 %[1]d 次代碼提交從 %[2]s
合併至 %[3]s
pulls.tab_conversation=對話內容
@@ -537,7 +541,7 @@ activity.merged_prs_label=已合併
activity.closed_issue_label=已關閉
activity.new_issues_count_1=建立問題
-search=搜尋
+contributors.contribution_type.commits=提交歷史
settings=儲存庫設定
settings.desc=設定是您可以管理儲存庫設定的地方
@@ -639,6 +643,8 @@ release.downloads=下載附件
+[graphs]
+
[org]
org_name_holder=組織名稱
org_full_name_holder=組織全名
@@ -693,6 +699,7 @@ dashboard=控制面版
organizations=組織管理
repositories=儲存庫管理
config=應用設定管理
+config_settings=組織設定
notices=系統提示管理
monitor=應用監控面版
first_page=首頁
@@ -755,8 +762,6 @@ repos.repo_manage_panel=儲存庫管理
repos.owner=所有者
repos.name=儲存庫名稱
repos.private=私有庫
-repos.watches=關註數
-repos.stars=讚好數
repos.issues=問題數
repos.size=大小
@@ -804,7 +809,6 @@ auths.tip.oauth2_provider=OAuth2 提供者
auths.tip.dropbox=建立新 App 在 https://www.dropbox.com/developers/apps
auths.tip.facebook=`在 https://developers.facebook.com/apps 註冊一個新的應用,並且新增一個產品 "Facebook Login"`
auths.tip.github=在 https://github.com/settings/applications/new 註冊一個新的 OAuth 應用程式
-auths.tip.gitlab=在 https://gitlab.com/profile/applications 註冊一個新的應用程式
auths.tip.openid_connect=使用 OpenID 連接探索 URL (/.well-known/openid-configuration) 來指定節點
auths.delete=刪除認證來源
auths.delete_auth_title=刪除認證來源
@@ -915,6 +919,7 @@ notices.desc=描述
notices.op=操作
notices.delete_success=已刪除系統提示。
+
[action]
create_repo=建立了儲存庫 %s
rename_repo=重新命名儲存庫 %[1]s
為 %[3]s
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index ea79c45674..0447a7d8b7 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -125,6 +125,15 @@ concept_user_organization=組織
name=名稱
value=值
+filter=篩選
+filter.is_archived=已封存
+filter.is_template=模板
+filter.public=公開
+filter.private=私有
+
+
+[search]
+
[aria]
navbar=導航列
footer=頁尾
@@ -292,7 +301,6 @@ collaborative_repos=參與協作的儲存庫
my_orgs=我的組織
my_mirrors=我的鏡像
view_home=訪問 %s
-search_repos=搜尋儲存庫...
filter=其他篩選條件
filter_by_team_repositories=以團隊儲存庫篩選
feed_of=「%s」的訊息來源
@@ -313,19 +321,7 @@ issues.in_your_repos=在您的儲存庫中
repos=儲存庫
users=使用者
organizations=組織
-search=搜尋
code=程式碼
-search.type.tooltip=搜尋類型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似關鍵字的結果
-search.match=符合
-search.match.tooltip=只包含完全符合關鍵字的結果
-code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。
-repo_no_results=沒有找到符合的儲存庫。
-user_no_results=沒有找到符合的使用者。
-org_no_results=沒有找到符合的組織。
-code_no_results=找不到符合您關鍵字的原始碼。
-code_search_results=「%s」的搜尋結果
code_last_indexed_at=最後索引 %s
relevant_repositories_tooltip=已隱藏缺少主題、圖示、說明、Fork 的儲存庫。
relevant_repositories=只顯示相關的儲存庫,顯示未篩選的結果。
@@ -341,7 +337,6 @@ remember_me=記得這個裝置
forgot_password_title=忘記密碼
forgot_password=忘記密碼?
sign_up_now=還沒有帳戶?馬上註冊。
-confirmation_mail_sent_prompt=新的確認信已發送至 %s。請在 %s內檢查您的收件匣並完成註冊作業。
must_change_password=更新您的密碼
allow_password_change=要求使用者更改密碼 (推薦)
reset_password_mail_sent_prompt=確認信已發送至 %s。請在 %s內檢查您的收件匣並完成帳戶救援作業。
@@ -555,6 +550,7 @@ org_still_own_packages=此組織仍然擁有一個以上的套件,請先刪除
target_branch_not_exist=目標分支不存在
+
[user]
change_avatar=更改大頭貼...
repositories=儲存庫
@@ -577,6 +573,7 @@ form.name_reserved=「%s」是保留的帳號。
form.name_pattern_not_allowed=帳號不可包含字元「%s」。
form.name_chars_not_allowed=帳號「%s」包含無效字元。
+
[settings]
profile=個人資料
account=帳戶
@@ -706,7 +703,6 @@ gpg_invalid_token_signature=提供的 GPG 金鑰、簽署、Token 不符合或 T
gpg_token_required=您必須為下列的 Token 提供簽署
gpg_token=Token
gpg_token_help=您可以使用以下方法產生簽署:
-gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
gpg_token_signature=Armored GPG 簽署
key_signature_gpg_placeholder=以「-----BEGIN PGP SIGNATURE-----」開頭
verify_gpg_key_success=已驗證 GPG 金鑰「%s」。
@@ -866,7 +862,6 @@ already_forked=您已經 fork 過 %s
fork_to_different_account=Fork 到其他帳戶
fork_visibility_helper=無法更改 fork 儲存庫的瀏覽權限。
use_template=使用此範本
-clone_in_vsc=在 VS Code 中 Clone
download_zip=下載 ZIP
download_tar=下載 TAR.GZ
download_bundle=下載 BUNDLE
@@ -1154,9 +1149,7 @@ commits.desc=瀏覽原始碼修改歷程。
commits.commits=次程式碼提交
commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。
commits.nothing_to_compare=這些分支是相同的。
-commits.search=搜尋提交歷史...
commits.search.tooltip=你可以用「author:」、「committer:」、「after:」、「before:」等作為關鍵字的前綴,例如: 「revert author:Alice before:2019-01-13」。
-commits.find=搜尋
commits.search_all=所有分支
commits.author=作者
commits.message=備註
@@ -1206,7 +1199,6 @@ projects.type.basic_kanban=基本看板
projects.type.bug_triage=Bug 檢傷分類
projects.template.desc=範本
projects.template.desc_helper=選擇專案範本以開始
-projects.type.uncategorized=未分類
projects.column.edit=編輯欄位
projects.column.edit_title=名稱
projects.column.new_title=名稱
@@ -1215,7 +1207,6 @@ projects.column.new=新增欄位
projects.column.set_default=設為預設
projects.column.set_default_desc=將此欄位設定為未分類問題及合併請求的預設預設值
projects.column.delete=刪除欄位
-projects.column.deletion_desc=刪除專案欄位會將所有相關的問題移動到「未分類」,是否繼續?
projects.column.color=顏色
projects.open=開啟
projects.close=關閉
@@ -1551,7 +1542,6 @@ pulls.compare_compare=拉取自
pulls.switch_comparison_type=切換比較類型
pulls.switch_head_and_base=切換 head 和 base
pulls.filter_branch=過濾分支
-pulls.no_results=未找到結果
pulls.nothing_to_compare=這些分支的內容相同,無需建立合併請求。
pulls.nothing_to_compare_and_allow_empty_pr=這些分支的內容相同,此合併請求將會是空白的。
pulls.has_pull_request=`已有介於這些分支間的合併請求:%[2]s#%[3]d`
@@ -1776,16 +1766,7 @@ activity.git_stats_and_deletions=和
activity.git_stats_deletion_1=刪除 %d 行
activity.git_stats_deletion_n=刪除 %d 行
-search=搜尋
-search.search_repo=搜尋儲存庫
-search.type.tooltip=搜尋類型
-search.fuzzy=模糊
-search.fuzzy.tooltip=包含近似關鍵字的結果
-search.match=符合
-search.match.tooltip=只包含完全符合關鍵字的結果
-search.results=在 %s 中搜尋 "%s" 的结果
-search.code_no_results=找不到符合您關鍵字的原始碼。
-search.code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。
+contributors.contribution_type.commits=提交歷史
settings=設定
settings.desc=設定是您可以管理儲存庫設定的地方
@@ -1847,6 +1828,7 @@ settings.pulls.default_allow_edits_from_maintainers=預設允許維護者進行
settings.releases_desc=啟用儲存庫版本發佈
settings.packages_desc=啟用儲存庫套件註冊中心
settings.projects_desc=啟用儲存庫專案
+settings.projects_mode_all=所有專案
settings.actions_desc=啟用儲存庫 Actions
settings.admin_settings=管理員設定
settings.admin_enable_health_check=啟用儲存庫的健康檢查 (git fsck)
@@ -1919,7 +1901,6 @@ settings.delete_collaborator=移除
settings.collaborator_deletion=移除協作者
settings.collaborator_deletion_desc=移除協作者將拒絕他存取此儲存庫。是否繼續?
settings.remove_collaborator_success=已移除協作者。
-settings.search_user_placeholder=搜尋使用者...
settings.org_not_allowed_to_be_collaborator=不可加入組織為協作者。
settings.change_team_access_not_allowed=只有組織擁有者可修改團隊的儲存庫存取權限
settings.team_not_in_organization=團隊和儲存庫不在相同的組織內
@@ -1927,7 +1908,6 @@ settings.teams=團隊
settings.add_team=增加團隊
settings.add_team_duplicate=團隊已擁有該儲存庫
settings.add_team_success=團隊現在可存取該儲存庫了。
-settings.search_team=搜尋團隊...
settings.change_team_permission_tip=團隊權限可於團隊設定頁面修改,不能針對儲存庫分別調整。
settings.delete_team_tip=此團隊可存取所有儲存庫,無法移除
settings.remove_team_success=已移除團隊存取儲存庫的權限。
@@ -2074,9 +2054,7 @@ settings.protect_whitelist_committers=使用白名單控管推送
settings.protect_whitelist_committers_desc=僅允許白名單內的使用者或團隊推送至該分支(但不可使用force push)。
settings.protect_whitelist_deploy_keys=將擁有寫入權限的部署金鑰加入白名單。
settings.protect_whitelist_users=允許推送的使用者:
-settings.protect_whitelist_search_users=搜尋使用者...
settings.protect_whitelist_teams=允許推送的團隊:
-settings.protect_whitelist_search_teams=搜尋團隊...
settings.protect_merge_whitelist_committers=啟用合併白名單
settings.protect_merge_whitelist_committers_desc=僅允許白名單內的使用者或團隊將合併請求合併至該分支。
settings.protect_merge_whitelist_users=允許合併的使用者:
@@ -2321,6 +2299,8 @@ error.csv.too_large=無法渲染此檔案,因為它太大了。
error.csv.unexpected=無法渲染此檔案,因為它包含了未預期的字元,於第 %d 行第 %d 列。
error.csv.invalid_field_count=無法渲染此檔案,因為它第 %d 行的欄位數量有誤。
+[graphs]
+
[org]
org_name_holder=組織名稱
org_full_name_holder=組織全名
@@ -2422,7 +2402,6 @@ teams.write_permission_desc=這個團隊擁有寫入 權限:
teams.admin_permission_desc=這個團隊擁有管理員 權限:成員可以讀取、推送和增加協作者到儲存庫。
teams.create_repo_permission_desc=此外,這個團隊還擁有建立儲存庫的權限:成員可以在組織中新增儲存庫。
teams.repositories=團隊儲存庫
-teams.search_repo_placeholder=搜尋儲存庫...
teams.remove_all_repos_title=移除所有團隊儲存庫
teams.remove_all_repos_desc=這將從團隊中移除所有儲存庫。
teams.add_all_repos_title=增加所有儲存庫
@@ -2450,6 +2429,8 @@ hooks=Webhook
authentication=認證來源
emails=使用者電子信箱
config=組態
+config_summary=摘要
+config_settings=設定
notices=系統提示
monitor=應用監控面版
first_page=首頁
@@ -2458,7 +2439,6 @@ total=總計:%d
dashboard.new_version_hint=現已推出 Gitea %s,您正在執行 %s。詳情請參閱部落格的說明。
dashboard.statistic=摘要
-dashboard.operations=維護作業
dashboard.system_status=系統狀態
dashboard.operation_name=作業名稱
dashboard.operation_switch=開關
@@ -2611,9 +2591,6 @@ repos.unadopted.no_more=找不到其他未接管的儲存庫
repos.owner=擁有者
repos.name=名稱
repos.private=私有
-repos.watches=關注數
-repos.stars=星號數
-repos.forks=Fork 數
repos.issues=問題數
repos.size=大小
@@ -2732,7 +2709,6 @@ auths.tip.nextcloud=在您的執行個體中,於選單「設定 -> 安全性 -
auths.tip.dropbox=建立新的 App。網址:https://www.dropbox.com/developers/apps
auths.tip.facebook=註冊新的應用程式並新增產品「Facebook 登入」。網址:https://developers.facebook.com/apps
auths.tip.github=註冊新的 OAuth 應用程式。網址:https://github.com/settings/applications/new
-auths.tip.gitlab=註冊新的應用程式。網址:https://gitlab.com/profile/applications
auths.tip.google_plus=從 Google API 控制台取得 OAuth2 用戶端憑證。網址:https://console.developers.google.com/
auths.tip.openid_connect=使用 OpenID 連接探索 URL (/.well-known/openid-configuration) 來指定節點
auths.tip.twitter=建立應用程式並確保有啟用「Allow this application to be used to Sign in with Twitter」。網址:https://dev.twitter.com/apps
@@ -2933,6 +2909,7 @@ notices.desc=描述
notices.op=操作
notices.delete_success=已刪除系統提示。
+
[action]
create_repo=建立了儲存庫 %s
rename_repo=重新命名儲存庫 %[1]s
為 %[3]s
@@ -3109,6 +3086,8 @@ pypi.requires=需要 Python
pypi.install=執行下列命令以使用 pip 安裝此套件:
rpm.registry=透過下列命令設定此註冊中心:
rpm.install=執行下列命令安裝此套件:
+rpm.repository=儲存庫資訊
+rpm.repository.architectures=架構
rubygems.install=執行下列命令以使用 gem 安裝此套件:
rubygems.install2=或將它加到 Gemfile:
rubygems.dependencies.runtime=執行階段相依性
diff --git a/package-lock.json b/package-lock.json
index 6918dc64b7..35bf886fc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,85 +5,97 @@
"packages": {
"": {
"dependencies": {
- "@citation-js/core": "0.7.6",
- "@citation-js/plugin-bibtex": "0.7.8",
- "@citation-js/plugin-csl": "0.7.6",
+ "@citation-js/core": "0.7.9",
+ "@citation-js/plugin-bibtex": "0.7.9",
+ "@citation-js/plugin-csl": "0.7.9",
"@citation-js/plugin-software-formats": "0.6.1",
- "@claviska/jquery-minicolors": "2.3.6",
- "@github/markdown-toolbar-element": "2.2.1",
- "@github/relative-time-element": "4.3.1",
+ "@github/markdown-toolbar-element": "2.2.3",
+ "@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
- "@primer/octicons": "19.8.0",
- "@webcomponents/custom-elements": "1.6.0",
+ "@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
- "asciinema-player": "3.6.3",
- "clippie": "4.0.6",
- "css-loader": "6.10.0",
+ "asciinema-player": "3.7.1",
+ "chart.js": "4.4.2",
+ "chartjs-adapter-dayjs-4": "1.0.4",
+ "chartjs-plugin-zoom": "2.0.1",
+ "clippie": "4.0.7",
+ "css-loader": "7.0.0",
+ "dayjs": "1.11.10",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
- "esbuild-loader": "4.0.3",
+ "esbuild-loader": "4.1.0",
"escape-goat": "4.0.0",
"fast-glob": "3.3.2",
- "htmx.org": "1.9.10",
+ "htmx.org": "1.9.11",
+ "idiomorph": "0.3.0",
"jquery": "3.7.1",
- "katex": "0.16.9",
+ "katex": "0.16.10",
"license-checker-webpack-plugin": "0.2.1",
- "mermaid": "10.7.0",
- "mini-css-extract-plugin": "2.8.0",
- "minimatch": "9.0.3",
- "monaco-editor": "0.45.0",
+ "mermaid": "10.9.0",
+ "mini-css-extract-plugin": "2.8.1",
+ "minimatch": "9.0.4",
+ "monaco-editor": "0.47.0",
"monaco-editor-webpack-plugin": "7.1.0",
- "pdfobject": "2.2.12",
+ "pdfobject": "2.3.0",
+ "postcss": "8.4.38",
+ "postcss-loader": "8.1.1",
+ "postcss-nesting": "12.1.1",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.2",
- "swagger-ui-dist": "5.11.2",
+ "swagger-ui-dist": "5.13.0",
+ "tailwindcss": "3.4.3",
+ "temporal-polyfill": "0.2.3",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tippy.js": "6.3.7",
"toastify-js": "1.12.0",
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
- "vue": "3.4.15",
+ "vanilla-colorful": "0.7.2",
+ "vue": "3.4.21",
"vue-bar-graph": "2.0.0",
+ "vue-chartjs": "5.3.0",
"vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5",
- "webpack": "5.90.1",
+ "webpack": "5.91.0",
"webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
- "@playwright/test": "1.41.1",
- "@stoplight/spectral-cli": "6.11.0",
- "@stylistic/eslint-plugin-js": "1.5.4",
- "@stylistic/stylelint-plugin": "2.0.0",
- "@vitejs/plugin-vue": "5.0.3",
- "eslint": "8.56.0",
+ "@playwright/test": "1.42.1",
+ "@stoplight/spectral-cli": "6.11.1",
+ "@stylistic/eslint-plugin-js": "1.7.0",
+ "@stylistic/stylelint-plugin": "2.1.1",
+ "@vitejs/plugin-vue": "5.0.4",
+ "eslint": "8.57.0",
"eslint-plugin-array-func": "4.0.0",
+ "eslint-plugin-github": "4.10.2",
"eslint-plugin-i": "2.29.1",
"eslint-plugin-jquery": "1.5.1",
"eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-use-extend-native": "0.5.0",
- "eslint-plugin-regexp": "2.2.0",
- "eslint-plugin-sonarjs": "0.23.0",
- "eslint-plugin-unicorn": "50.0.1",
- "eslint-plugin-vitest": "0.3.21",
- "eslint-plugin-vitest-globals": "1.4.0",
- "eslint-plugin-vue": "9.21.1",
- "eslint-plugin-vue-scoped-css": "2.7.2",
+ "eslint-plugin-regexp": "2.4.0",
+ "eslint-plugin-sonarjs": "0.25.1",
+ "eslint-plugin-unicorn": "52.0.0",
+ "eslint-plugin-vitest": "0.4.1",
+ "eslint-plugin-vitest-globals": "1.5.0",
+ "eslint-plugin-vue": "9.24.0",
+ "eslint-plugin-vue-scoped-css": "2.8.0",
"eslint-plugin-wc": "2.0.4",
- "jsdom": "24.0.0",
+ "happy-dom": "14.5.0",
"markdownlint-cli": "0.39.0",
"postcss-html": "1.6.0",
- "stylelint": "16.2.1",
+ "stylelint": "16.3.1",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.4",
+ "stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.2.0",
- "updates": "15.1.1",
- "vite-string-plugin": "1.1.3",
- "vitest": "1.2.2"
+ "updates": "16.0.0",
+ "vite-string-plugin": "1.1.5",
+ "vitest": "1.4.0"
},
"engines": {
"node": ">= 18.0.0"
@@ -98,6 +110,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@asyncapi/specs": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-4.3.1.tgz",
@@ -108,107 +131,34 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.23.5",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
- "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
- "dev": true,
+ "version": "7.24.2",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
+ "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
"dependencies": {
- "@babel/highlight": "^7.23.4",
- "chalk": "^2.4.2"
+ "@babel/highlight": "^7.24.2",
+ "picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
- "node_modules/@babel/code-frame/node_modules/ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "dependencies": {
- "color-convert": "^1.9.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/code-frame/node_modules/chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/code-frame/node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/@babel/code-frame/node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "dev": true
- },
- "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/@babel/code-frame/node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/@babel/code-frame/node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
- "dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
- "version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
- "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
- "dev": true,
+ "version": "7.24.2",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
+ "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
- "js-tokens": "^4.0.0"
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
@@ -218,7 +168,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -230,7 +179,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -244,7 +192,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
"dependencies": {
"color-name": "1.1.3"
}
@@ -252,14 +199,12 @@
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "dev": true
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
"engines": {
"node": ">=0.8.0"
}
@@ -268,7 +213,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -276,14 +220,12 @@
"node_modules/@babel/highlight/node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
@@ -292,9 +234,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.23.9",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
- "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
+ "version": "7.24.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
+ "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -303,9 +245,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.23.9",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
- "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
+ "version": "7.24.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
+ "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -319,9 +261,9 @@
"integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
},
"node_modules/@citation-js/core": {
- "version": "0.7.6",
- "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.6.tgz",
- "integrity": "sha512-qbB6RjwSsx/AjlCSAqoWKN05VxpjADYe8GmnPJnRB7QeNiVmqaRc8NSQDdvQ+4qhCkQOtMH15Sa2Nde4cvlXhw==",
+ "version": "0.7.9",
+ "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.9.tgz",
+ "integrity": "sha512-fSbkB32JayDChZnAYC/kB+sWHRvxxL7ibVetyBOyzOc+5aCnjb6UVsbcfhnkOIEyAMoRRvWDyFmakEoTtA5ttQ==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2",
@@ -349,9 +291,9 @@
}
},
"node_modules/@citation-js/plugin-bibtex": {
- "version": "0.7.8",
- "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.8.tgz",
- "integrity": "sha512-20fUXe1zm1oCONFflGj3mgIk6DHspPjWrBirGfsyHmVSR/4xqnSbrqtztLiV15zt3tbKLepTaHm3ZTrcLOK0MA==",
+ "version": "0.7.9",
+ "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.9.tgz",
+ "integrity": "sha512-gIJpCd6vmmTOcRfDrSOjtoNhw2Mi94UwFxmgJ7GwkXyTYcNheW5VlMMo1tlqjakJGARQ0eOsKcI57gSPqJSS2g==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2",
@@ -377,9 +319,9 @@
}
},
"node_modules/@citation-js/plugin-csl": {
- "version": "0.7.6",
- "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.6.tgz",
- "integrity": "sha512-H/dhzU56+D71Hzjto1x9PDtvsWaiI+Dx6Jj1vjiFtCCnbU/Zvqo5xFZNPstee+hFE6AsJ2xYlI8QujrGH+V1pQ==",
+ "version": "0.7.9",
+ "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.9.tgz",
+ "integrity": "sha512-mbD7CnUiPOuVnjeJwo+d0RGUcY0PE8n01gHyjq0qpTeS42EGmQ9+LzqfsTUVWWBndTwc6zLRuIF1qFAUHKE4oA==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"citeproc": "^2.4.6"
@@ -453,18 +395,10 @@
"node": ">=14.0.0"
}
},
- "node_modules/@claviska/jquery-minicolors": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/@claviska/jquery-minicolors/-/jquery-minicolors-2.3.6.tgz",
- "integrity": "sha512-8Ro6D4GCrmOl41+6w4NFhEOpx8vjxwVRI69bulXsFDt49uVRKhLU5TnzEV7AmOJrylkVq+ugnYNMiGHBieeKUQ==",
- "peerDependencies": {
- "jquery": ">= 1.7.x"
- }
- },
"node_modules/@csstools/css-parser-algorithms": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz",
- "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==",
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
+ "integrity": "sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA==",
"dev": true,
"funding": [
{
@@ -480,13 +414,13 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-tokenizer": "^2.2.3"
+ "@csstools/css-tokenizer": "^2.2.4"
}
},
"node_modules/@csstools/css-tokenizer": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz",
- "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==",
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz",
+ "integrity": "sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw==",
"dev": true,
"funding": [
{
@@ -503,9 +437,9 @@
}
},
"node_modules/@csstools/media-query-list-parser": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz",
- "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==",
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz",
+ "integrity": "sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA==",
"dev": true,
"funding": [
{
@@ -521,15 +455,35 @@
"node": "^14 || ^16 || >=18"
},
"peerDependencies": {
- "@csstools/css-parser-algorithms": "^2.5.0",
- "@csstools/css-tokenizer": "^2.2.3"
+ "@csstools/css-parser-algorithms": "^2.6.1",
+ "@csstools/css-tokenizer": "^2.2.4"
+ }
+ },
+ "node_modules/@csstools/selector-resolve-nested": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz",
+ "integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^6.0.13"
}
},
"node_modules/@csstools/selector-specificity": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz",
- "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==",
- "dev": true,
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz",
+ "integrity": "sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q==",
"funding": [
{
"type": "github",
@@ -555,10 +509,20 @@
"node": ">=10.0.0"
}
},
+ "node_modules/@dual-bundle/import-meta-resolve": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz",
+ "integrity": "sha512-ZKXyJeFAzcpKM2kk8ipoGIPUqx9BX52omTGnfwjJvxOCaZTM2wtDK7zN0aIgPRbT9XYAlha0HtmZ+XKteuh0Gw==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
- "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
+ "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
@@ -571,9 +535,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
- "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
+ "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
@@ -586,9 +550,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
- "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
+ "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
@@ -601,9 +565,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
- "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
+ "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
@@ -616,9 +580,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
- "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
+ "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
@@ -631,9 +595,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
- "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
+ "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
@@ -646,9 +610,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
- "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
+ "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
@@ -661,9 +625,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
- "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
+ "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
@@ -676,9 +640,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
- "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
+ "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
@@ -691,9 +655,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
- "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
+ "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
@@ -706,9 +670,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
- "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
+ "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
@@ -721,9 +685,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
- "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
+ "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
@@ -736,9 +700,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
- "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
+ "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
@@ -751,9 +715,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
- "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
+ "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
@@ -766,9 +730,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
- "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
+ "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
@@ -781,9 +745,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
- "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
+ "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
@@ -796,9 +760,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
- "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
+ "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
@@ -811,9 +775,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
- "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
@@ -826,9 +790,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
- "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
+ "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
@@ -841,9 +805,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
- "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
+ "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
@@ -856,9 +820,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
- "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
+ "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
@@ -871,9 +835,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
- "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
+ "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
@@ -886,9 +850,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
- "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
+ "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
@@ -1008,28 +972,34 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
- "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
+ "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@github/browserslist-config": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@github/browserslist-config/-/browserslist-config-1.0.0.tgz",
+ "integrity": "sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==",
+ "dev": true
+ },
"node_modules/@github/combobox-nav": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.3.1.tgz",
"integrity": "sha512-gwxPzLw8XKecy1nP63i9lOBritS3bWmxl02UX6G0TwMQZbMem1BCS1tEZgYd3mkrkiDrUMWaX+DbFCuDFo3K+A=="
},
"node_modules/@github/markdown-toolbar-element": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.1.tgz",
- "integrity": "sha512-ap+ulyqzG3aVqwKsKjbDdYwM75TQXZpPtmIuPwm+54OTgcC96267oX3cEqd1wSqGsH7O5PonZ//fE9jH7Q4JkA=="
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.2.3.tgz",
+ "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
},
"node_modules/@github/relative-time-element": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.3.1.tgz",
- "integrity": "sha512-zL79nlhZVCg7x2Pf/HT5MB0mowmErE71VXpF10/3Wy8dQwkninNO1M9aOizh2wKC5LkSpDXqNYjDZwbH0/bcSg=="
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.0.tgz",
+ "integrity": "sha512-CrI6oAecoahG7PF5dsgjdvlF5kCtusVMjg810EULD81TvnDsP+k/FRi/ClFubWLgBo4EGpr2EfvmumtqQFo7ow=="
},
"node_modules/@github/text-expander-element": {
"version": "2.6.1",
@@ -1089,16 +1059,15 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
- "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"dev": true
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -1115,7 +1084,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -1127,7 +1095,6 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -1135,17 +1102,10 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true
- },
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -1162,7 +1122,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -1177,7 +1136,6 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -1203,41 +1161,41 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
- "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dependencies": {
- "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
- "@jridgewell/trace-mapping": "^0.3.9"
+ "@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
- "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
- "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
- "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dependencies": {
- "@jridgewell/gen-mapping": "^0.3.0",
- "@jridgewell/trace-mapping": "^0.3.9"
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
@@ -1246,9 +1204,9 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.22",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
- "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -1278,6 +1236,11 @@
"jsep": "^0.4.0||^1.0.0"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+ },
"node_modules/@mcaptcha/core-glue": {
"version": "0.1.0-alpha-5",
"resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz",
@@ -1363,19 +1326,30 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
+ "node_modules/@pkgr/core": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
+ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
"node_modules/@playwright/test": {
- "version": "1.41.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz",
- "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
+ "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"dev": true,
"dependencies": {
- "playwright": "1.41.1"
+ "playwright": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@@ -1394,9 +1368,9 @@
}
},
"node_modules/@primer/octicons": {
- "version": "19.8.0",
- "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.8.0.tgz",
- "integrity": "sha512-Imze/fyW41Io5fN+27T5EAeXJrgBjMbz6nzU+wYbRylXvIAjLPUvaJPVoStiFlgSU+TjTUJqg5A9rgMDzTyMCg==",
+ "version": "19.9.0",
+ "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.9.0.tgz",
+ "integrity": "sha512-uAZa9cMgWkzbEsZnYWB7tg0vt7QprubD7ljtprz2fBJ8CjyqoxFRRsFvH4UiJdjK/3o87ODgDkhiflyJXDh+Lg==",
"dependencies": {
"object-assign": "^4.1.1"
}
@@ -1446,9 +1420,9 @@
"dev": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
- "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz",
+ "integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==",
"cpu": [
"arm"
],
@@ -1459,9 +1433,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz",
- "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz",
+ "integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==",
"cpu": [
"arm64"
],
@@ -1472,9 +1446,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz",
- "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz",
+ "integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==",
"cpu": [
"arm64"
],
@@ -1485,9 +1459,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz",
- "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz",
+ "integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==",
"cpu": [
"x64"
],
@@ -1498,9 +1472,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz",
- "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz",
+ "integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==",
"cpu": [
"arm"
],
@@ -1511,9 +1485,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz",
- "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz",
+ "integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==",
"cpu": [
"arm64"
],
@@ -1524,9 +1498,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz",
- "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz",
+ "integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==",
"cpu": [
"arm64"
],
@@ -1536,10 +1510,23 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz",
+ "integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==",
+ "cpu": [
+ "ppc64le"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz",
- "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz",
+ "integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==",
"cpu": [
"riscv64"
],
@@ -1549,10 +1536,23 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz",
+ "integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz",
- "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz",
+ "integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==",
"cpu": [
"x64"
],
@@ -1563,9 +1563,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz",
- "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz",
+ "integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==",
"cpu": [
"x64"
],
@@ -1576,9 +1576,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz",
- "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz",
+ "integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==",
"cpu": [
"arm64"
],
@@ -1589,9 +1589,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz",
- "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz",
+ "integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==",
"cpu": [
"ia32"
],
@@ -1602,9 +1602,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz",
- "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz",
+ "integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==",
"cpu": [
"x64"
],
@@ -1712,9 +1712,9 @@
}
},
"node_modules/@stoplight/spectral-cli": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.11.0.tgz",
- "integrity": "sha512-IURDN47BPIf3q4ZyUPujGpBzuHWFE5yT34w9rTJ1GKA4SgdscEdQO9KoTjOPT4G4cvDlEV3bNxwQ3uRm7+wRlA==",
+ "version": "6.11.1",
+ "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.11.1.tgz",
+ "integrity": "sha512-1zqsQ0TOuVSnxxZ9mHBfC0IygV6ex7nAY6Mp59mLmw5fW103U9yPVK5ZcX9ZngCmr3PdteAnMDUIIaoDGso6nA==",
"dev": true,
"dependencies": {
"@stoplight/json": "~3.21.0",
@@ -1735,7 +1735,7 @@
"pony-cause": "^1.0.0",
"stacktracey": "^2.1.7",
"tslib": "^2.3.0",
- "yargs": "17.3.1"
+ "yargs": "~17.7.2"
},
"bin": {
"spectral": "dist/index.js"
@@ -1899,20 +1899,33 @@
}
},
"node_modules/@stoplight/spectral-parsers": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.3.tgz",
- "integrity": "sha512-J0KW5Rh5cHWnJQ3yN+cr/ijNFVirPSR0pkQbdrNX30VboEl083UEDrQ3yov9kjLVIWEk9t9kKE7Eo3QT/k4JLA==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.4.tgz",
+ "integrity": "sha512-nCTVvtX6q71M8o5Uvv9kxU31Gk1TRmgD6/k8HBhdCmKG6FWcwgjiZouA/R3xHLn/VwTI/9k8SdG5Mkdy0RBqbQ==",
"dev": true,
"dependencies": {
"@stoplight/json": "~3.21.0",
- "@stoplight/types": "^13.6.0",
- "@stoplight/yaml": "~4.2.3",
+ "@stoplight/types": "^14.1.1",
+ "@stoplight/yaml": "~4.3.0",
"tslib": "^2.3.1"
},
"engines": {
"node": "^12.20 || >=14.13"
}
},
+ "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz",
+ "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.4",
+ "utility-types": "^3.10.0"
+ },
+ "engines": {
+ "node": "^12.20 || >=14.13"
+ }
+ },
"node_modules/@stoplight/spectral-ref-resolver": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.4.tgz",
@@ -1981,6 +1994,27 @@
"node": ">=12"
}
},
+ "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz",
+ "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==",
+ "dev": true,
+ "dependencies": {
+ "@stoplight/ordered-object-literal": "^1.0.1",
+ "@stoplight/types": "^13.0.0",
+ "@stoplight/yaml-ast-parser": "0.0.48",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.8"
+ }
+ },
+ "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml-ast-parser": {
+ "version": "0.0.48",
+ "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz",
+ "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==",
+ "dev": true
+ },
"node_modules/@stoplight/spectral-rulesets": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.18.1.tgz",
@@ -2051,14 +2085,14 @@
}
},
"node_modules/@stoplight/yaml": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.2.3.tgz",
- "integrity": "sha512-Mx01wjRAR9C7yLMUyYFTfbUf5DimEpHMkRDQ1PKLe9dfNILbgdxyrncsOXM3vCpsQ1Hfj4bPiGl+u4u6e9Akqw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz",
+ "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==",
"dev": true,
"dependencies": {
- "@stoplight/ordered-object-literal": "^1.0.1",
- "@stoplight/types": "^13.0.0",
- "@stoplight/yaml-ast-parser": "0.0.48",
+ "@stoplight/ordered-object-literal": "^1.0.5",
+ "@stoplight/types": "^14.1.1",
+ "@stoplight/yaml-ast-parser": "0.0.50",
"tslib": "^2.2.0"
},
"engines": {
@@ -2066,17 +2100,31 @@
}
},
"node_modules/@stoplight/yaml-ast-parser": {
- "version": "0.0.48",
- "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.48.tgz",
- "integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==",
+ "version": "0.0.50",
+ "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz",
+ "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==",
"dev": true
},
- "node_modules/@stylistic/eslint-plugin-js": {
- "version": "1.5.4",
- "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.5.4.tgz",
- "integrity": "sha512-3ctWb3NvJNV1MsrZN91cYp2EGInLPSoZKphXIbIRx/zjZxKwLDr9z4LMOWtqjq14li/OgqUUcMq5pj8fgbLoTw==",
+ "node_modules/@stoplight/yaml/node_modules/@stoplight/types": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz",
+ "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==",
"dev": true,
"dependencies": {
+ "@types/json-schema": "^7.0.4",
+ "utility-types": "^3.10.0"
+ },
+ "engines": {
+ "node": "^12.20 || >=14.13"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin-js": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.7.0.tgz",
+ "integrity": "sha512-PN6On/+or63FGnhhMKSQfYcWutRlzOiYlVdLM6yN7lquoBTqUJHYnl4TA4MHwiAt46X5gRxDr1+xPZ1lOLcL+Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/eslint": "^8.56.2",
"acorn": "^8.11.3",
"escape-string-regexp": "^4.0.0",
"eslint-visitor-keys": "^3.4.3",
@@ -2090,19 +2138,19 @@
}
},
"node_modules/@stylistic/stylelint-plugin": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.0.0.tgz",
- "integrity": "sha512-dHKuT6PGd1WGZLOTuozAM7GdQzdmlmnFXYzvV1jYJXXpcCpV/OJ3+n8TXpMkoOeKHpJydY43EOoZTO1W/FOA4Q==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-2.1.1.tgz",
+ "integrity": "sha512-xqHTmQZN7EbnFDW7jw0rAsdFNO4IRqvXhrh3qhUlIwF/x09Zm7kgs/ADktHxsTJYcw346PpGihsB0t4pZhpeHw==",
"dev": true,
"dependencies": {
- "@csstools/css-parser-algorithms": "^2.3.2",
- "@csstools/css-tokenizer": "^2.2.1",
- "@csstools/media-query-list-parser": "^2.1.5",
+ "@csstools/css-parser-algorithms": "^2.5.0",
+ "@csstools/css-tokenizer": "^2.2.3",
+ "@csstools/media-query-list-parser": "^2.1.7",
"is-plain-object": "^5.0.0",
- "postcss-selector-parser": "^6.0.13",
+ "postcss-selector-parser": "^6.0.15",
"postcss-value-parser": "^4.2.0",
"style-search": "^0.1.0",
- "stylelint": "^16.0.2"
+ "stylelint": "^16.2.1"
},
"engines": {
"node": "^18.12 || >=20.9"
@@ -2169,9 +2217,9 @@
}
},
"node_modules/@types/eslint": {
- "version": "8.56.2",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
- "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
+ "version": "8.56.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.7.tgz",
+ "integrity": "sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
@@ -2196,6 +2244,12 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
"node_modules/@types/marked": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz",
@@ -2215,9 +2269,9 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
- "version": "20.11.14",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.14.tgz",
- "integrity": "sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==",
+ "version": "20.12.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz",
+ "integrity": "sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -2235,9 +2289,9 @@
"dev": true
},
"node_modules/@types/semver": {
- "version": "7.5.6",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
- "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/tern": {
@@ -2259,30 +2313,120 @@
"integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==",
"dev": true
},
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
- "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz",
+ "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/visitor-keys": "6.21.0"
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "7.5.0",
+ "@typescript-eslint/type-utils": "7.5.0",
+ "@typescript-eslint/utils": "7.5.0",
+ "@typescript-eslint/visitor-keys": "7.5.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^7.0.0",
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz",
+ "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "7.5.0",
+ "@typescript-eslint/types": "7.5.0",
+ "@typescript-eslint/typescript-estree": "7.5.0",
+ "@typescript-eslint/visitor-keys": "7.5.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz",
+ "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "7.5.0",
+ "@typescript-eslint/visitor-keys": "7.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz",
+ "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "7.5.0",
+ "@typescript-eslint/utils": "7.5.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@typescript-eslint/types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
- "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz",
+ "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==",
"dev": true,
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -2290,13 +2434,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
- "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz",
+ "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/visitor-keys": "6.21.0",
+ "@typescript-eslint/types": "7.5.0",
+ "@typescript-eslint/visitor-keys": "7.5.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2305,7 +2449,7 @@
"ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -2317,42 +2461,57 @@
}
}
},
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@typescript-eslint/utils": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
- "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz",
+ "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "6.21.0",
- "@typescript-eslint/types": "6.21.0",
- "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/scope-manager": "7.5.0",
+ "@typescript-eslint/types": "7.5.0",
+ "@typescript-eslint/typescript-estree": "7.5.0",
"semver": "^7.5.4"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^7.0.0 || ^8.0.0"
+ "eslint": "^8.56.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
- "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz",
+ "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/types": "7.5.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
- "node": "^16.0.0 || >=18.0.0"
+ "node": "^18.18.0 || >=20.0.0"
},
"funding": {
"type": "opencollective",
@@ -2366,9 +2525,9 @@
"dev": true
},
"node_modules/@vitejs/plugin-vue": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz",
- "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==",
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz",
+ "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==",
"dev": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
@@ -2379,13 +2538,13 @@
}
},
"node_modules/@vitest/expect": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
- "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
+ "integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
"dev": true,
"dependencies": {
- "@vitest/spy": "1.2.2",
- "@vitest/utils": "1.2.2",
+ "@vitest/spy": "1.4.0",
+ "@vitest/utils": "1.4.0",
"chai": "^4.3.10"
},
"funding": {
@@ -2393,12 +2552,12 @@
}
},
"node_modules/@vitest/runner": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
- "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
+ "integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
"dev": true,
"dependencies": {
- "@vitest/utils": "1.2.2",
+ "@vitest/utils": "1.4.0",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -2434,9 +2593,9 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
- "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
+ "integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -2448,9 +2607,9 @@
}
},
"node_modules/@vitest/snapshot/node_modules/magic-string": {
- "version": "0.30.7",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
- "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+ "version": "0.30.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+ "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2460,9 +2619,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
- "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
+ "integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -2472,9 +2631,9 @@
}
},
"node_modules/@vitest/utils": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
- "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
+ "integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -2502,46 +2661,46 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
- "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
+ "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
"dependencies": {
- "@babel/parser": "^7.23.6",
- "@vue/shared": "3.4.15",
+ "@babel/parser": "^7.23.9",
+ "@vue/shared": "3.4.21",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
- "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
+ "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
"dependencies": {
- "@vue/compiler-core": "3.4.15",
- "@vue/shared": "3.4.15"
+ "@vue/compiler-core": "3.4.21",
+ "@vue/shared": "3.4.21"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
- "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
+ "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
"dependencies": {
- "@babel/parser": "^7.23.6",
- "@vue/compiler-core": "3.4.15",
- "@vue/compiler-dom": "3.4.15",
- "@vue/compiler-ssr": "3.4.15",
- "@vue/shared": "3.4.15",
+ "@babel/parser": "^7.23.9",
+ "@vue/compiler-core": "3.4.21",
+ "@vue/compiler-dom": "3.4.21",
+ "@vue/compiler-ssr": "3.4.21",
+ "@vue/shared": "3.4.21",
"estree-walker": "^2.0.2",
- "magic-string": "^0.30.5",
- "postcss": "^8.4.33",
+ "magic-string": "^0.30.7",
+ "postcss": "^8.4.35",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-sfc/node_modules/magic-string": {
- "version": "0.30.6",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz",
- "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==",
+ "version": "0.30.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+ "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
@@ -2550,62 +2709,62 @@
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
- "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
+ "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
"dependencies": {
- "@vue/compiler-dom": "3.4.15",
- "@vue/shared": "3.4.15"
+ "@vue/compiler-dom": "3.4.21",
+ "@vue/shared": "3.4.21"
}
},
"node_modules/@vue/reactivity": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
- "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
+ "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
"dependencies": {
- "@vue/shared": "3.4.15"
+ "@vue/shared": "3.4.21"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
- "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
+ "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
"dependencies": {
- "@vue/reactivity": "3.4.15",
- "@vue/shared": "3.4.15"
+ "@vue/reactivity": "3.4.21",
+ "@vue/shared": "3.4.21"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
- "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
+ "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
"dependencies": {
- "@vue/runtime-core": "3.4.15",
- "@vue/shared": "3.4.15",
+ "@vue/runtime-core": "3.4.21",
+ "@vue/shared": "3.4.21",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
- "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
+ "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
"dependencies": {
- "@vue/compiler-ssr": "3.4.15",
- "@vue/shared": "3.4.15"
+ "@vue/compiler-ssr": "3.4.21",
+ "@vue/shared": "3.4.21"
},
"peerDependencies": {
- "vue": "3.4.15"
+ "vue": "3.4.21"
}
},
"node_modules/@vue/shared": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
- "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
+ "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
},
"node_modules/@webassemblyjs/ast": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
- "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
+ "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
"dependencies": {
"@webassemblyjs/helper-numbers": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6"
@@ -2622,9 +2781,9 @@
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
},
"node_modules/@webassemblyjs/helper-buffer": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
- "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
+ "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.11.6",
@@ -2642,14 +2801,14 @@
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
},
"node_modules/@webassemblyjs/helper-wasm-section": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
- "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
+ "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
"dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-buffer": "1.12.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/wasm-gen": "1.11.6"
+ "@webassemblyjs/wasm-gen": "1.12.1"
}
},
"node_modules/@webassemblyjs/ieee754": {
@@ -2674,26 +2833,26 @@
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
},
"node_modules/@webassemblyjs/wasm-edit": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
- "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
+ "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
"dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-buffer": "1.12.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
- "@webassemblyjs/helper-wasm-section": "1.11.6",
- "@webassemblyjs/wasm-gen": "1.11.6",
- "@webassemblyjs/wasm-opt": "1.11.6",
- "@webassemblyjs/wasm-parser": "1.11.6",
- "@webassemblyjs/wast-printer": "1.11.6"
+ "@webassemblyjs/helper-wasm-section": "1.12.1",
+ "@webassemblyjs/wasm-gen": "1.12.1",
+ "@webassemblyjs/wasm-opt": "1.12.1",
+ "@webassemblyjs/wasm-parser": "1.12.1",
+ "@webassemblyjs/wast-printer": "1.12.1"
}
},
"node_modules/@webassemblyjs/wasm-gen": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
- "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
+ "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
"dependencies": {
- "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/ast": "1.12.1",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
"@webassemblyjs/ieee754": "1.11.6",
"@webassemblyjs/leb128": "1.11.6",
@@ -2701,22 +2860,22 @@
}
},
"node_modules/@webassemblyjs/wasm-opt": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
- "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
+ "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
"dependencies": {
- "@webassemblyjs/ast": "1.11.6",
- "@webassemblyjs/helper-buffer": "1.11.6",
- "@webassemblyjs/wasm-gen": "1.11.6",
- "@webassemblyjs/wasm-parser": "1.11.6"
+ "@webassemblyjs/ast": "1.12.1",
+ "@webassemblyjs/helper-buffer": "1.12.1",
+ "@webassemblyjs/wasm-gen": "1.12.1",
+ "@webassemblyjs/wasm-parser": "1.12.1"
}
},
"node_modules/@webassemblyjs/wasm-parser": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
- "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
+ "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
"dependencies": {
- "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/ast": "1.12.1",
"@webassemblyjs/helper-api-error": "1.11.6",
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
"@webassemblyjs/ieee754": "1.11.6",
@@ -2725,19 +2884,14 @@
}
},
"node_modules/@webassemblyjs/wast-printer": {
- "version": "1.11.6",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
- "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
+ "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
"dependencies": {
- "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/ast": "1.12.1",
"@xtuc/long": "4.2.2"
}
},
- "node_modules/@webcomponents/custom-elements": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz",
- "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww=="
- },
"node_modules/@webpack-cli/configtest": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
@@ -2852,18 +3006,6 @@
"webpack": ">=5"
}
},
- "node_modules/agent-base": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
- "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==",
- "dev": true,
- "dependencies": {
- "debug": "^4.3.4"
- },
- "engines": {
- "node": ">= 14"
- }
- },
"node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
@@ -2959,19 +3101,53 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
- "node_modules/array-buffer-byte-length": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
- "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "is-array-buffer": "^3.0.1"
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+ "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2985,6 +3161,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/array-includes": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz",
+ "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -2994,18 +3190,75 @@
"node": ">=8"
}
},
- "node_modules/arraybuffer.prototype.slice": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
- "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz",
+ "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+ "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
"dev": true,
"dependencies": {
- "array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1",
- "get-intrinsic": "^1.2.1",
- "is-array-buffer": "^3.0.2",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+ "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+ "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.2.1",
+ "get-intrinsic": "^1.2.3",
+ "is-array-buffer": "^3.0.4",
"is-shared-array-buffer": "^1.0.2"
},
"engines": {
@@ -3025,9 +3278,9 @@
}
},
"node_modules/asciinema-player": {
- "version": "3.6.3",
- "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.6.3.tgz",
- "integrity": "sha512-62aDgLpbuduhmpFfNgPOzf6fOluACLsftVnjpWJjUXX6dqhqTckFqWoJ+ayA0XjSlZ9l9wXTcJqRqvAAIpMblg==",
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.7.1.tgz",
+ "integrity": "sha512-zDJteGjBzNQhHEnD0aG7GqV3E53sOyKb1WCxKNRm2PquU70Lq3s4xxb91wyDS0hBJ3J/TB8aY3y8gjGPN+T23A==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"solid-js": "^1.3.0"
@@ -3054,6 +3307,12 @@
"node": ">=4"
}
},
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true
+ },
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -3072,12 +3331,6 @@
"astring": "bin/astring"
}
},
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true
- },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -3091,10 +3344,13 @@
}
},
"node_modules/available-typed-arrays": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
- "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
"engines": {
"node": ">= 0.4"
},
@@ -3102,6 +3358,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axe-core": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
+ "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
+ "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3134,6 +3408,17 @@
"node": "*"
}
},
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3160,9 +3445,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.22.3",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
- "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
+ "version": "4.23.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+ "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"funding": [
{
"type": "opencollective",
@@ -3178,8 +3463,8 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001580",
- "electron-to-chromium": "^1.4.648",
+ "caniuse-lite": "^1.0.30001587",
+ "electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
@@ -3246,14 +3531,19 @@
}
},
"node_modules/call-bind": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
- "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dev": true,
"dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
"function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.1",
- "set-function-length": "^1.1.1"
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3263,15 +3553,22 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
"engines": {
"node": ">=6"
}
},
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/caniuse-lite": {
- "version": "1.0.30001581",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
- "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
+ "version": "1.0.30001605",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz",
+ "integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==",
"funding": [
{
"type": "opencollective",
@@ -3329,6 +3626,40 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz",
+ "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chartjs-adapter-dayjs-4": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs-4/-/chartjs-adapter-dayjs-4-1.0.4.tgz",
+ "integrity": "sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "chart.js": ">=4.0.1",
+ "dayjs": "^1.9.7"
+ }
+ },
+ "node_modules/chartjs-plugin-zoom": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz",
+ "integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==",
+ "dependencies": {
+ "hammerjs": "^2.0.8"
+ },
+ "peerDependencies": {
+ "chart.js": ">=3.2.0"
+ }
+ },
"node_modules/check-error": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -3341,6 +3672,40 @@
"node": "*"
}
},
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@@ -3391,9 +3756,9 @@
}
},
"node_modules/clippie": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.6.tgz",
- "integrity": "sha512-E5EtOw8iMH0enuL3kBZJ+Po1nPnBD7O+HHpIaWpfWgHbHmdoOQoERrlNOcEEn2yMJQ98WqeKacouAcnRXn7oWA=="
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.7.tgz",
+ "integrity": "sha512-xmIARCRFQUoCR0kNNu4uIv5f/IFqM1fUts0vQwt1hQEdCPEqs3/dTaG38WenlWOgs3Fcn73PBYXbPIVSlOgFRw=="
},
"node_modules/cliui": {
"version": "7.0.4",
@@ -3487,18 +3852,6 @@
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -3528,12 +3881,12 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/core-js-compat": {
- "version": "3.35.1",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
- "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
+ "version": "3.36.1",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
+ "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==",
"dev": true,
"dependencies": {
- "browserslist": "^4.22.2"
+ "browserslist": "^4.23.0"
},
"funding": {
"type": "opencollective",
@@ -3552,7 +3905,6 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
- "dev": true,
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
@@ -3608,21 +3960,21 @@
}
},
"node_modules/css-loader": {
- "version": "6.10.0",
- "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz",
- "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.0.0.tgz",
+ "integrity": "sha512-WrO4FVoamxt5zY9CauZjoJgXRi/LZKIk+Ta7YvpSGr5r/eMYPNp5/T9ODlMe4/1rF5DYlycG1avhV4g3A/tiAw==",
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.33",
- "postcss-modules-extract-imports": "^3.0.0",
- "postcss-modules-local-by-default": "^4.0.4",
- "postcss-modules-scope": "^3.1.1",
+ "postcss-modules-extract-imports": "^3.1.0",
+ "postcss-modules-local-by-default": "^4.0.5",
+ "postcss-modules-scope": "^3.2.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.5.4"
},
"engines": {
- "node": ">= 12.13.0"
+ "node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
@@ -3630,7 +3982,7 @@
},
"peerDependencies": {
"@rspack/core": "0.x || 1.x",
- "webpack": "^5.0.0"
+ "webpack": "^5.27.0"
},
"peerDependenciesMeta": {
"@rspack/core": {
@@ -3726,18 +4078,6 @@
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"dev": true
},
- "node_modules/cssstyle": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
- "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==",
- "dev": true,
- "dependencies": {
- "rrweb-cssom": "^0.6.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3766,34 +4106,10 @@
"cytoscape": "^3.2.0"
}
},
- "node_modules/cytoscape-fcose": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
- "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
- "dependencies": {
- "cose-base": "^2.2.0"
- },
- "peerDependencies": {
- "cytoscape": "^3.2.0"
- }
- },
- "node_modules/cytoscape-fcose/node_modules/cose-base": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
- "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
- "dependencies": {
- "layout-base": "^2.0.0"
- }
- },
- "node_modules/cytoscape-fcose/node_modules/layout-base": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
- "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="
- },
"node_modules/d3": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
- "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
"dependencies": {
"d3-array": "3",
"d3-axis": "3",
@@ -3998,9 +4314,9 @@
}
},
"node_modules/d3-geo": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz",
- "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
"dependencies": {
"d3-array": "2.5.0 - 3"
},
@@ -4110,9 +4426,9 @@
}
},
"node_modules/d3-scale-chromatic": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
- "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
"dependencies": {
"d3-color": "1 - 3",
"d3-interpolate": "1 - 3"
@@ -4212,23 +4528,67 @@
"lodash-es": "^4.17.21"
}
},
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
"node_modules/data-uri-to-buffer": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
"integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
"dev": true
},
- "node_modules/data-urls": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
- "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "node_modules/data-view-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
+ "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
"dev": true,
"dependencies": {
- "whatwg-mimetype": "^4.0.0",
- "whatwg-url": "^14.0.0"
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
},
"engines": {
- "node": ">=18"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
+ "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
+ "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/dayjs": {
@@ -4252,12 +4612,6 @@
}
}
},
- "node_modules/decimal.js": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
- "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
- "dev": true
- },
"node_modules/decode-named-character-reference": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
@@ -4307,17 +4661,20 @@
"dev": true
},
"node_modules/define-data-property": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
- "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"dependencies": {
- "get-intrinsic": "^1.2.1",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0"
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-properties": {
@@ -4345,15 +4702,6 @@
"robust-predicates": "^3.0.2"
}
},
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/dependency-graph": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
@@ -4371,10 +4719,15 @@
"node": ">=6"
}
},
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+ },
"node_modules/diff": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
- "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"engines": {
"node": ">=0.3.1"
}
@@ -4400,6 +4753,11 @@
"node": ">=8"
}
},
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+ },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -4454,9 +4812,9 @@
}
},
"node_modules/dompurify": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.8.tgz",
- "integrity": "sha512-b7uwreMYL2eZhrSCRC4ahLTeZcPZxSmYfmcQGXGkXiZSNW1X85v+SDM5KsWcpivIiUBH47Ji7NtyUdpLeF5JZQ=="
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz",
+ "integrity": "sha512-Fan4uMuyB26gFV3ovPoEoQbxRRPfTu3CvImyZnhGq5fsIEO+gEFLp45ISFt+kQBWsK5ulDdT0oV28jS1UrwQLg=="
},
"node_modules/domutils": {
"version": "3.1.0",
@@ -4484,8 +4842,7 @@
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/easymde": {
"version": "2.18.0",
@@ -4500,19 +4857,19 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.653",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.653.tgz",
- "integrity": "sha512-wA2A2LQCqnEwQAvwADQq3KpMpNwgAUBnRmrFgRzHnPhbQUFArTR32Ab46f4p0MovDLcg4uqd4nCsN2hTltslpA=="
+ "version": "1.4.727",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.727.tgz",
+ "integrity": "sha512-brpv4KTeC4g0Fx2FeIKytLd4UGn1zBQq5Lauy7zEWT9oqkaj5mgsxblEZIAOf1HHLlXxzr6adGViiBy5Z39/CA=="
},
"node_modules/elkjs": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.1.tgz",
- "integrity": "sha512-JWKDyqAdltuUcyxaECtYG6H4sqysXSLeoXuGUBfRNESMTkj+w+qdb0jya8Z/WI0jVd03WQtCGhS6FOFtlhD5FQ=="
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.2.tgz",
+ "integrity": "sha512-2Y/RaA1pdgSHpY0YG4TYuYCD2wh97CRvu22eLG3Kz0pgQ/6KbIFTxsTnDc4MH/6hFlg2L/9qXrDMG0nMjP63iw=="
},
"node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/emojis-list": {
"version": "3.0.0",
@@ -4523,9 +4880,9 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.15.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
- "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+ "version": "5.16.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz",
+ "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -4549,15 +4906,14 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
- "dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/envinfo": {
- "version": "7.11.0",
- "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz",
- "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==",
+ "version": "7.11.1",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz",
+ "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==",
"bin": {
"envinfo": "dist/cli.js"
},
@@ -4569,56 +4925,62 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dev": true,
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-abstract": {
- "version": "1.22.3",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
- "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
+ "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==",
"dev": true,
"dependencies": {
- "array-buffer-byte-length": "^1.0.0",
- "arraybuffer.prototype.slice": "^1.0.2",
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.5",
- "es-set-tostringtag": "^2.0.1",
+ "array-buffer-byte-length": "^1.0.1",
+ "arraybuffer.prototype.slice": "^1.0.3",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "data-view-buffer": "^1.0.1",
+ "data-view-byte-length": "^1.0.1",
+ "data-view-byte-offset": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-set-tostringtag": "^2.0.3",
"es-to-primitive": "^1.2.1",
"function.prototype.name": "^1.1.6",
- "get-intrinsic": "^1.2.2",
- "get-symbol-description": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "get-symbol-description": "^1.0.2",
"globalthis": "^1.0.3",
"gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.0",
- "has-proto": "^1.0.1",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.3",
"has-symbols": "^1.0.3",
- "hasown": "^2.0.0",
- "internal-slot": "^1.0.5",
- "is-array-buffer": "^3.0.2",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.0.7",
+ "is-array-buffer": "^3.0.4",
"is-callable": "^1.2.7",
- "is-negative-zero": "^2.0.2",
+ "is-data-view": "^1.0.1",
+ "is-negative-zero": "^2.0.3",
"is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.2",
+ "is-shared-array-buffer": "^1.0.3",
"is-string": "^1.0.7",
- "is-typed-array": "^1.1.12",
+ "is-typed-array": "^1.1.13",
"is-weakref": "^1.0.2",
"object-inspect": "^1.13.1",
"object-keys": "^1.1.1",
- "object.assign": "^4.1.4",
- "regexp.prototype.flags": "^1.5.1",
- "safe-array-concat": "^1.0.1",
- "safe-regex-test": "^1.0.0",
- "string.prototype.trim": "^1.2.8",
- "string.prototype.trimend": "^1.0.7",
- "string.prototype.trimstart": "^1.0.7",
- "typed-array-buffer": "^1.0.0",
- "typed-array-byte-length": "^1.0.0",
- "typed-array-byte-offset": "^1.0.0",
- "typed-array-length": "^1.0.4",
+ "object.assign": "^4.1.5",
+ "regexp.prototype.flags": "^1.5.2",
+ "safe-array-concat": "^1.1.2",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.trim": "^1.2.9",
+ "string.prototype.trimend": "^1.0.8",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.2",
+ "typed-array-byte-length": "^1.0.1",
+ "typed-array-byte-offset": "^1.0.2",
+ "typed-array-length": "^1.0.6",
"unbox-primitive": "^1.0.2",
- "which-typed-array": "^1.1.13"
+ "which-typed-array": "^1.1.15"
},
"engines": {
"node": ">= 0.4"
@@ -4628,19 +4990,19 @@
}
},
"node_modules/es-aggregate-error": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.11.tgz",
- "integrity": "sha512-DCiZiNlMlbvofET/cE55My387NiLvuGToBEZDdK9U2G3svDCjL8WOgO5Il6lO83nQ8qmag/R9nArdpaFQ/m3lA==",
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz",
+ "integrity": "sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A==",
"dev": true,
"dependencies": {
- "define-data-property": "^1.1.0",
+ "define-data-property": "^1.1.4",
"define-properties": "^1.2.1",
- "es-abstract": "^1.22.1",
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
"globalthis": "^1.0.3",
- "has-property-descriptors": "^1.0.0",
- "set-function-name": "^2.0.1"
+ "has-property-descriptors": "^1.0.2",
+ "set-function-name": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -4649,25 +5011,92 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/es-module-lexer": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
- "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w=="
- },
- "node_modules/es-set-tostringtag": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
- "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"dependencies": {
- "get-intrinsic": "^1.2.2",
- "has-tostringtag": "^1.0.0",
- "hasown": "^2.0.0"
+ "get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.0.18",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz",
+ "integrity": "sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.0",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "globalthis": "^1.0.3",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.3",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.7",
+ "iterator.prototype": "^1.1.2",
+ "safe-array-concat": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz",
+ "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw=="
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+ "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+ "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+ "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ }
+ },
"node_modules/es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
@@ -4686,9 +5115,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.19.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
- "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
+ "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@@ -4697,37 +5126,37 @@
"node": ">=12"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.19.12",
- "@esbuild/android-arm": "0.19.12",
- "@esbuild/android-arm64": "0.19.12",
- "@esbuild/android-x64": "0.19.12",
- "@esbuild/darwin-arm64": "0.19.12",
- "@esbuild/darwin-x64": "0.19.12",
- "@esbuild/freebsd-arm64": "0.19.12",
- "@esbuild/freebsd-x64": "0.19.12",
- "@esbuild/linux-arm": "0.19.12",
- "@esbuild/linux-arm64": "0.19.12",
- "@esbuild/linux-ia32": "0.19.12",
- "@esbuild/linux-loong64": "0.19.12",
- "@esbuild/linux-mips64el": "0.19.12",
- "@esbuild/linux-ppc64": "0.19.12",
- "@esbuild/linux-riscv64": "0.19.12",
- "@esbuild/linux-s390x": "0.19.12",
- "@esbuild/linux-x64": "0.19.12",
- "@esbuild/netbsd-x64": "0.19.12",
- "@esbuild/openbsd-x64": "0.19.12",
- "@esbuild/sunos-x64": "0.19.12",
- "@esbuild/win32-arm64": "0.19.12",
- "@esbuild/win32-ia32": "0.19.12",
- "@esbuild/win32-x64": "0.19.12"
+ "@esbuild/aix-ppc64": "0.20.2",
+ "@esbuild/android-arm": "0.20.2",
+ "@esbuild/android-arm64": "0.20.2",
+ "@esbuild/android-x64": "0.20.2",
+ "@esbuild/darwin-arm64": "0.20.2",
+ "@esbuild/darwin-x64": "0.20.2",
+ "@esbuild/freebsd-arm64": "0.20.2",
+ "@esbuild/freebsd-x64": "0.20.2",
+ "@esbuild/linux-arm": "0.20.2",
+ "@esbuild/linux-arm64": "0.20.2",
+ "@esbuild/linux-ia32": "0.20.2",
+ "@esbuild/linux-loong64": "0.20.2",
+ "@esbuild/linux-mips64el": "0.20.2",
+ "@esbuild/linux-ppc64": "0.20.2",
+ "@esbuild/linux-riscv64": "0.20.2",
+ "@esbuild/linux-s390x": "0.20.2",
+ "@esbuild/linux-x64": "0.20.2",
+ "@esbuild/netbsd-x64": "0.20.2",
+ "@esbuild/openbsd-x64": "0.20.2",
+ "@esbuild/sunos-x64": "0.20.2",
+ "@esbuild/win32-arm64": "0.20.2",
+ "@esbuild/win32-ia32": "0.20.2",
+ "@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/esbuild-loader": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.0.3.tgz",
- "integrity": "sha512-YpaSRisj7TSg6maKKKG9OJGGm0BZ7EXeov8J8cXEYdugjlAJ0wL7aj2JactoQvPJ113v2Ar204pdJWrZsAQc8Q==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.1.0.tgz",
+ "integrity": "sha512-543TtIvqbqouEMlOHg4xKoDQkmdImlwIpyAIgpUtDPvMuklU/c2k+Qt2O3VeDBgAwozxmlEbjOzV+F8CZ0g+Bw==",
"dependencies": {
- "esbuild": "^0.19.0",
+ "esbuild": "^0.20.0",
"get-tsconfig": "^4.7.0",
"loader-utils": "^2.0.4",
"webpack-sources": "^1.4.3"
@@ -4740,9 +5169,9 @@
}
},
"node_modules/escalade": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
- "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"engines": {
"node": ">=6"
}
@@ -4771,16 +5200,16 @@
}
},
"node_modules/eslint": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
- "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
+ "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.56.0",
- "@humanwhocodes/config-array": "^0.11.13",
+ "@eslint/js": "8.57.0",
+ "@humanwhocodes/config-array": "^0.11.14",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@@ -4826,9 +5255,9 @@
}
},
"node_modules/eslint-compat-utils": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.4.1.tgz",
- "integrity": "sha512-5N7ZaJG5pZxUeNNJfUchurLVrunD1xJvyg5kYOIVF8kg1f3ajTikmAu/5fZ9w100omNPOoMjngRszh/Q/uFGMg==",
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz",
+ "integrity": "sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==",
"dev": true,
"dependencies": {
"semver": "^7.5.4"
@@ -4840,6 +5269,18 @@
"eslint": ">=6.0.0"
}
},
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
"node_modules/eslint-import-resolver-node": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
@@ -4861,9 +5302,9 @@
}
},
"node_modules/eslint-module-utils": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
- "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
+ "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
"dev": true,
"dependencies": {
"debug": "^3.2.7"
@@ -4898,6 +5339,92 @@
"eslint": ">=8.40.0"
}
},
+ "node_modules/eslint-plugin-escompat": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-escompat/-/eslint-plugin-escompat-3.4.0.tgz",
+ "integrity": "sha512-ufTPv8cwCxTNoLnTZBFTQ5SxU2w7E7wiMIS7PSxsgP1eAxFjtSaoZ80LRn64hI8iYziE6kJG6gX/ZCJVxh48Bg==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.21.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.14.1"
+ }
+ },
+ "node_modules/eslint-plugin-eslint-comments": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz",
+ "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5",
+ "ignore": "^5.0.5"
+ },
+ "engines": {
+ "node": ">=6.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=4.19.1"
+ }
+ },
+ "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/eslint-plugin-filenames": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz",
+ "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==",
+ "dev": true,
+ "dependencies": {
+ "lodash.camelcase": "4.3.0",
+ "lodash.kebabcase": "4.1.1",
+ "lodash.snakecase": "4.1.1",
+ "lodash.upperfirst": "4.3.1"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ }
+ },
+ "node_modules/eslint-plugin-github": {
+ "version": "4.10.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-4.10.2.tgz",
+ "integrity": "sha512-F1F5aAFgi1Y5hYoTFzGQACBkw5W1hu2Fu5FSTrMlXqrojJnKl1S2pWO/rprlowRQpt+hzHhqSpsfnodJEVd5QA==",
+ "dev": true,
+ "dependencies": {
+ "@github/browserslist-config": "^1.0.0",
+ "@typescript-eslint/eslint-plugin": "^7.0.1",
+ "@typescript-eslint/parser": "^7.0.1",
+ "aria-query": "^5.3.0",
+ "eslint-config-prettier": ">=8.0.0",
+ "eslint-plugin-escompat": "^3.3.3",
+ "eslint-plugin-eslint-comments": "^3.2.0",
+ "eslint-plugin-filenames": "^1.3.2",
+ "eslint-plugin-i18n-text": "^1.0.1",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-no-only-tests": "^3.0.0",
+ "eslint-plugin-prettier": "^5.0.0",
+ "eslint-rule-documentation": ">=1.0.0",
+ "jsx-ast-utils": "^3.3.2",
+ "prettier": "^3.0.0",
+ "svg-element-attributes": "^1.3.1"
+ },
+ "bin": {
+ "eslint-ignore-errors": "bin/eslint-ignore-errors.js"
+ },
+ "peerDependencies": {
+ "eslint": "^8.0.1"
+ }
+ },
"node_modules/eslint-plugin-i": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-i/-/eslint-plugin-i-2.29.1.tgz",
@@ -4945,6 +5472,98 @@
"node": "*"
}
},
+ "node_modules/eslint-plugin-i18n-text": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz",
+ "integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==",
+ "dev": true,
+ "peerDependencies": {
+ "eslint": ">=5.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
+ "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.7",
+ "array.prototype.findlastindex": "^1.2.3",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.8.0",
+ "hasown": "^2.0.0",
+ "is-core-module": "^2.13.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.7",
+ "object.groupby": "^1.0.1",
+ "object.values": "^1.1.7",
+ "semver": "^6.3.1",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/eslint-plugin-jquery": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jquery/-/eslint-plugin-jquery-1.5.1.tgz",
@@ -4954,6 +5573,58 @@
"eslint": ">=5.4.0"
}
},
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
+ "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "aria-query": "^5.3.0",
+ "array-includes": "^3.1.7",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "=4.7.0",
+ "axobject-query": "^3.2.1",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "es-iterator-helpers": "^1.0.15",
+ "hasown": "^2.0.0",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.7",
+ "object.fromentries": "^2.0.7"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/eslint-plugin-no-jquery": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.7.0.tgz",
@@ -4963,6 +5634,15 @@
"eslint": ">=2.3.0"
}
},
+ "node_modules/eslint-plugin-no-only-tests": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.1.0.tgz",
+ "integrity": "sha512-Lf4YW/bL6Un1R6A76pRZyE1dl1vr31G/ev8UzIc/geCgFWyrKil8hVjYqWVKGB/UIGmb6Slzs9T0wNezdSVegw==",
+ "dev": true,
+ "engines": {
+ "node": ">=5.0.0"
+ }
+ },
"node_modules/eslint-plugin-no-use-extend-native": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-no-use-extend-native/-/eslint-plugin-no-use-extend-native-0.5.0.tgz",
@@ -4978,10 +5658,40 @@
"node": ">=6.0.0"
}
},
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
+ "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
+ "dev": true,
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.6"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": "*",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
"node_modules/eslint-plugin-regexp": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.2.0.tgz",
- "integrity": "sha512-0kwpiWiLRVBkVr3oIRQLl196sXP/NF6DQFefv9jtR4ZOgQR+6WID2pIZ0I+wIt54qgBPwBB7Gm2a+ueh8/WsFQ==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.4.0.tgz",
+ "integrity": "sha512-OL2S6VPjQhs9s/NclQ0qattVq1J0GU8ox70/HIVy5Dxw+qbbdd7KQkyucsez2clEQjvdtDe12DTnPphFFUyXFg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
@@ -5000,21 +5710,21 @@
}
},
"node_modules/eslint-plugin-sonarjs": {
- "version": "0.23.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.23.0.tgz",
- "integrity": "sha512-z44T3PBf9W7qQ/aR+NmofOTyg6HLhSEZOPD4zhStqBpLoMp8GYhFksuUBnCxbnf1nfISpKBVkQhiBLFI/F4Wlg==",
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.25.1.tgz",
+ "integrity": "sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==",
"dev": true,
"engines": {
- "node": ">=14"
+ "node": ">=16"
},
"peerDependencies": {
"eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/eslint-plugin-unicorn": {
- "version": "50.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
- "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
+ "version": "52.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
+ "integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
@@ -5045,12 +5755,12 @@
}
},
"node_modules/eslint-plugin-vitest": {
- "version": "0.3.21",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.3.21.tgz",
- "integrity": "sha512-oYwR1MrwaBw/OG6CKU+SJYleAc442w6CWL1RTQl5WLwy8X3sh0bgHIQk5iEtmTak3Q+XAvZglr0bIoDOjFdkcw==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vitest/-/eslint-plugin-vitest-0.4.1.tgz",
+ "integrity": "sha512-+PnZ2u/BS+f5FiuHXz4zKsHPcMKHie+K+1Uvu/x91ovkCMEOJqEI8E9Tw1Wzx2QRz4MHOBHYf1ypO8N1K0aNAA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/utils": "^6.20.0"
+ "@typescript-eslint/utils": "^7.4.0"
},
"engines": {
"node": "^18.0.0 || >= 20.0.0"
@@ -5069,22 +5779,23 @@
}
},
"node_modules/eslint-plugin-vitest-globals": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.4.0.tgz",
- "integrity": "sha512-WE+YlK9X9s4vf5EaYRU0Scw7WItDZStm+PapFSYlg2ABNtaQ4zIG7wEqpoUB3SlfM+SgkhgmzR0TeJOO5k3/Nw==",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vitest-globals/-/eslint-plugin-vitest-globals-1.5.0.tgz",
+ "integrity": "sha512-ZSsVOaOIig0oVLzRTyk8lUfBfqzWxr/J3/NFMfGGRIkGQPejJYmDH3gXmSJxAojts77uzAGB/UmVrwi2DC4LYA==",
"dev": true
},
"node_modules/eslint-plugin-vue": {
- "version": "9.21.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz",
- "integrity": "sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw==",
+ "version": "9.24.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz",
+ "integrity": "sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
+ "globals": "^13.24.0",
"natural-compare": "^1.4.0",
"nth-check": "^2.1.1",
- "postcss-selector-parser": "^6.0.13",
- "semver": "^7.5.4",
+ "postcss-selector-parser": "^6.0.15",
+ "semver": "^7.6.0",
"vue-eslint-parser": "^9.4.2",
"xml-name-validator": "^4.0.0"
},
@@ -5096,13 +5807,13 @@
}
},
"node_modules/eslint-plugin-vue-scoped-css": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.7.2.tgz",
- "integrity": "sha512-myJ99CJuwmAx5kq1WjgIeaUkxeU6PIEUh7age79Alm30bhN4fVTapOQLSMlvVTgxr36Y3igsZ3BCJM32LbHHig==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.8.0.tgz",
+ "integrity": "sha512-JXb3Um4+AhuDGxSX6FAGCI0p811xF7W8L7yxC8wmAEZEI/teTjlpC09noqQZHXn53RZ/TGQJ8Onaq4teYLxBbg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
- "eslint-compat-utils": "^0.4.0",
+ "eslint-compat-utils": "^0.5.0",
"lodash": "^4.17.21",
"postcss": "^8.4.31",
"postcss-safe-parser": "^6.0.0",
@@ -5134,6 +5845,15 @@
"eslint": ">=5"
}
},
+ "node_modules/eslint-rule-documentation": {
+ "version": "1.0.23",
+ "resolved": "https://registry.npmjs.org/eslint-rule-documentation/-/eslint-rule-documentation-1.0.23.tgz",
+ "integrity": "sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -5371,9 +6091,9 @@
}
},
"node_modules/fastq": {
- "version": "1.17.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz",
- "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==",
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dependencies": {
"reusify": "^1.0.4"
}
@@ -5405,25 +6125,6 @@
}
}
},
- "node_modules/fetch-ponyfill/node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
- },
- "node_modules/fetch-ponyfill/node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
- },
- "node_modules/fetch-ponyfill/node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5486,9 +6187,9 @@
}
},
"node_modules/flatted": {
- "version": "3.2.9",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
- "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
"node_modules/for-each": {
@@ -5504,7 +6205,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
- "dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
@@ -5516,20 +6216,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
- "dev": true,
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -5553,7 +6239,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -5628,16 +6313,20 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
- "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dev": true,
"dependencies": {
+ "es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
+ "engines": {
+ "node": ">= 0.4"
+ },
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -5686,13 +6375,14 @@
}
},
"node_modules/get-symbol-description": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
- "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+ "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
+ "call-bind": "^1.0.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
@@ -5702,9 +6392,9 @@
}
},
"node_modules/get-tsconfig": {
- "version": "4.7.2",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
- "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
+ "version": "4.7.3",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+ "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
@@ -5735,7 +6425,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -5896,6 +6585,28 @@
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
"integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ=="
},
+ "node_modules/hammerjs": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
+ "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/happy-dom": {
+ "version": "14.5.0",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.5.0.tgz",
+ "integrity": "sha512-KvOtCq7eamc7cjihM0F1wj6FptuXzooc3Typa7Vgu6ns2uKGXC4BIFlK80SdH2w8zcW0gtxpBVI/sUqbYtljDA==",
+ "dev": true,
+ "dependencies": {
+ "entities": "^4.5.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-mimetype": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -5914,21 +6625,21 @@
}
},
"node_modules/has-property-descriptors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
- "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"dependencies": {
- "get-intrinsic": "^1.2.2"
+ "es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
- "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"dev": true,
"engines": {
"node": ">= 0.4"
@@ -5950,12 +6661,12 @@
}
},
"node_modules/has-tostringtag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
- "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
- "has-symbols": "^1.0.2"
+ "has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
@@ -5970,9 +6681,9 @@
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="
},
"node_modules/hasown": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
- "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -6000,18 +6711,6 @@
"node": ">=14"
}
},
- "node_modules/html-encoding-sniffer": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
- "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
- "dev": true,
- "dependencies": {
- "whatwg-encoding": "^3.1.1"
- },
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@@ -6044,35 +6743,9 @@
}
},
"node_modules/htmx.org": {
- "version": "1.9.10",
- "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.10.tgz",
- "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA=="
- },
- "node_modules/http-proxy-agent": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
- "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
- "dev": true,
- "dependencies": {
- "agent-base": "^7.1.0",
- "debug": "^4.3.4"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "node_modules/https-proxy-agent": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
- "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
- "dev": true,
- "dependencies": {
- "agent-base": "^7.0.2",
- "debug": "4"
- },
- "engines": {
- "node": ">= 14"
- }
+ "version": "1.9.11",
+ "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.11.tgz",
+ "integrity": "sha512-WlVuICn8dfNOOgYmdYzYG8zSnP3++AdHkMHooQAzGZObWpVXYathpz/I37ycF4zikR6YduzfCvEcxk20JkIUsw=="
},
"node_modules/human-signals": {
"version": "5.0.0",
@@ -6105,6 +6778,11 @@
"postcss": "^8.1.0"
}
},
+ "node_modules/idiomorph": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz",
+ "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA=="
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -6125,9 +6803,9 @@
]
},
"node_modules/ignore": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
- "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true,
"engines": {
"node": ">= 4"
@@ -6147,7 +6825,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -6210,21 +6887,21 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
- "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz",
+ "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==",
"dev": true,
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/internal-slot": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
- "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+ "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
"dev": true,
"dependencies": {
- "get-intrinsic": "^1.2.2",
+ "es-errors": "^1.3.0",
"hasown": "^2.0.0",
"side-channel": "^1.0.4"
},
@@ -6249,14 +6926,16 @@
}
},
"node_modules/is-array-buffer": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
- "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+ "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.0",
- "is-typed-array": "^1.1.10"
+ "get-intrinsic": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -6265,8 +6944,22 @@
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
- "dev": true
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+ },
+ "node_modules/is-async-function": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+ "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/is-bigint": {
"version": "1.0.4",
@@ -6280,6 +6973,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@@ -6334,6 +7038,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-data-view": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
+ "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
+ "dev": true,
+ "dependencies": {
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-date-object": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
@@ -6357,6 +7076,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-finalizationregistry": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
+ "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -6365,6 +7096,21 @@
"node": ">=8"
}
},
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-get-set-prop": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-get-set-prop/-/is-get-set-prop-1.0.0.tgz",
@@ -6395,10 +7141,22 @@
"js-types": "^1.0.0"
}
},
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-negative-zero": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
- "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true,
"engines": {
"node": ">= 0.4"
@@ -6499,13 +7257,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-shared-array-buffer": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
- "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2"
+ "call-bind": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -6554,12 +7327,12 @@
}
},
"node_modules/is-typed-array": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
- "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+ "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
"dev": true,
"dependencies": {
- "which-typed-array": "^1.1.11"
+ "which-typed-array": "^1.1.14"
},
"engines": {
"node": ">= 0.4"
@@ -6577,6 +7350,18 @@
"is-potential-custom-element-name": "^1.0.0"
}
},
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-weakref": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -6589,6 +7374,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-weakset": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
+ "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -6608,11 +7409,23 @@
"node": ">=0.10.0"
}
},
+ "node_modules/iterator.prototype": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
+ "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "reflect.getprototypeof": "^1.0.4",
+ "set-function-name": "^2.0.1"
+ }
+ },
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
- "dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@@ -6653,6 +7466,14 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/jiti": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
+ "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@@ -6665,9 +7486,9 @@
"dev": true
},
"node_modules/js-tokens": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.2.tgz",
- "integrity": "sha512-Olnt+V7xYdvGze9YTbGFZIfQXuGV4R3nQwwl8BrtgaPE/wq8UFpUHWuTNc05saowhSr1ZO6tx+V6RjE9D5YQog==",
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz",
+ "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==",
"dev": true
},
"node_modules/js-types": {
@@ -6699,55 +7520,6 @@
"node": ">=12.0.0"
}
},
- "node_modules/jsdom": {
- "version": "24.0.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz",
- "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==",
- "dev": true,
- "dependencies": {
- "cssstyle": "^4.0.1",
- "data-urls": "^5.0.0",
- "decimal.js": "^10.4.3",
- "form-data": "^4.0.0",
- "html-encoding-sniffer": "^4.0.0",
- "http-proxy-agent": "^7.0.0",
- "https-proxy-agent": "^7.0.2",
- "is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.7",
- "parse5": "^7.1.2",
- "rrweb-cssom": "^0.6.0",
- "saxes": "^6.0.0",
- "symbol-tree": "^3.2.4",
- "tough-cookie": "^4.1.3",
- "w3c-xmlserializer": "^5.0.0",
- "webidl-conversions": "^7.0.0",
- "whatwg-encoding": "^3.1.1",
- "whatwg-mimetype": "^4.0.0",
- "whatwg-url": "^14.0.0",
- "ws": "^8.16.0",
- "xml-name-validator": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "canvas": "^2.11.2"
- },
- "peerDependenciesMeta": {
- "canvas": {
- "optional": true
- }
- }
- },
- "node_modules/jsdom/node_modules/xml-name-validator": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
- "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
- "dev": true,
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/jsep": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.8.tgz",
@@ -6838,15 +7610,30 @@
"node": ">=0.10.0"
}
},
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/just-extend": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
"integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ=="
},
"node_modules/katex": {
- "version": "0.16.9",
- "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz",
- "integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==",
+ "version": "0.16.10",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
+ "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
@@ -6889,11 +7676,29 @@
}
},
"node_modules/known-css-properties": {
- "version": "0.29.0",
- "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz",
- "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
+ "version": "0.30.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.30.0.tgz",
+ "integrity": "sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==",
"dev": true
},
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/layout-base": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
@@ -6981,11 +7786,18 @@
"node": ">=8"
}
},
+ "node_modules/lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/linkify-it": {
"version": "5.0.0",
@@ -7063,12 +7875,30 @@
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
"integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA=="
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "dev": true
+ },
+ "node_modules/lodash.kebabcase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+ "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+ "dev": true
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "node_modules/lodash.snakecase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+ "dev": true
+ },
"node_modules/lodash.sortedlastindex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/lodash.sortedlastindex/-/lodash.sortedlastindex-4.1.0.tgz",
@@ -7104,6 +7934,12 @@
"integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
"dev": true
},
+ "node_modules/lodash.upperfirst": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+ "dev": true
+ },
"node_modules/loupe": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
@@ -7208,16 +8044,16 @@
}
},
"node_modules/markdownlint-cli/node_modules/glob": {
- "version": "10.3.10",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
- "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+ "version": "10.3.12",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+ "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
- "jackspeak": "^2.3.5",
+ "jackspeak": "^2.3.6",
"minimatch": "^9.0.1",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
- "path-scurry": "^1.10.1"
+ "minipass": "^7.0.4",
+ "path-scurry": "^1.10.2"
},
"bin": {
"glob": "dist/esm/bin.mjs"
@@ -7316,9 +8152,9 @@
"dev": true
},
"node_modules/meow": {
- "version": "13.1.0",
- "resolved": "https://registry.npmjs.org/meow/-/meow-13.1.0.tgz",
- "integrity": "sha512-o5R/R3Tzxq0PJ3v3qcQJtSvSE9nKOLSAaDuuoMzDVuGTwHdccMWcYomh9Xolng2tjT6O/Y83d+0coVGof6tqmA==",
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+ "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
"dev": true,
"engines": {
"node": ">=18"
@@ -7341,22 +8177,22 @@
}
},
"node_modules/mermaid": {
- "version": "10.7.0",
- "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.7.0.tgz",
- "integrity": "sha512-PsvGupPCkN1vemAAjScyw4pw34p4/0dZkSrqvAB26hUvJulOWGIwt35FZWmT9wPIi4r0QLa5X0PB4YLIGn0/YQ==",
+ "version": "10.9.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.0.tgz",
+ "integrity": "sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==",
"dependencies": {
"@braintree/sanitize-url": "^6.0.1",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
- "cytoscape": "^3.23.0",
+ "cytoscape": "^3.28.1",
"cytoscape-cose-bilkent": "^4.1.0",
- "cytoscape-fcose": "^2.1.0",
"d3": "^7.4.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.10",
"dayjs": "^1.11.7",
"dompurify": "^3.0.5",
"elkjs": "^0.9.0",
+ "katex": "^0.16.9",
"khroma": "^2.0.0",
"lodash-es": "^4.17.21",
"mdast-util-from-markdown": "^1.3.0",
@@ -7841,9 +8677,9 @@
}
},
"node_modules/mini-css-extract-plugin": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.0.tgz",
- "integrity": "sha512-CxmUYPFcTgET1zImteG/LZOy/4T5rTojesQXkSNBiquhydn78tfbCE9sjIjnJ/UcjNjOC1bphTCCW5rrS7cXAg==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz",
+ "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==",
"dependencies": {
"schema-utils": "^4.0.0",
"tapable": "^2.2.1"
@@ -7860,9 +8696,9 @@
}
},
"node_modules/minimatch": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
- "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+ "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -7886,15 +8722,14 @@
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
- "dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mlly": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz",
- "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
+ "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
@@ -7904,9 +8739,9 @@
}
},
"node_modules/monaco-editor": {
- "version": "0.45.0",
- "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz",
- "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA=="
+ "version": "0.47.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz",
+ "integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw=="
},
"node_modules/monaco-editor-webpack-plugin": {
"version": "7.1.0",
@@ -7938,6 +8773,16 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -8014,25 +8859,6 @@
}
}
},
- "node_modules/node-fetch/node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
- },
- "node_modules/node-fetch/node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
- },
- "node_modules/node-fetch/node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -8081,15 +8907,14 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/npm-run-path": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
- "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
"dev": true,
"dependencies": {
"path-key": "^4.0.0"
@@ -8125,12 +8950,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
- "node_modules/nwsapi": {
- "version": "2.2.7",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
- "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==",
- "dev": true
- },
"node_modules/obj-props": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/obj-props/-/obj-props-1.4.0.tgz",
@@ -8148,6 +8967,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -8184,6 +9011,69 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object.entries": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz",
+ "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
+ "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -8266,7 +9156,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -8278,7 +9167,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "dev": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
@@ -8303,18 +9191,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/parse5": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
- "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
- "dev": true,
- "dependencies": {
- "entities": "^4.4.0"
- },
- "funding": {
- "url": "https://github.com/inikulin/parse5?sponsor=1"
- }
- },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8345,12 +9221,11 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
- "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
- "dev": true,
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
+ "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
"dependencies": {
- "lru-cache": "^9.1.1 || ^10.0.0",
+ "lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
@@ -8364,7 +9239,6 @@
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
"integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
- "dev": true,
"engines": {
"node": "14 || >=16.14"
}
@@ -8394,9 +9268,9 @@
}
},
"node_modules/pdfobject": {
- "version": "2.2.12",
- "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.2.12.tgz",
- "integrity": "sha512-D0oyD/sj8j82AMaJhoyMaY1aD5TkbpU3FbJC6w9/cpJlZRpYHqAkutXw1Ca/FKjYPZmTAu58uGIfgOEaDlbY8A=="
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pdfobject/-/pdfobject-2.3.0.tgz",
+ "integrity": "sha512-w/9pXDXTDs3IDmOri/w8lM/w6LHR0/F4fcBLLzH+4csSoyshQ5su0TE7k0FLHZO7aOjVLDGecqd1M89+PVpVAA=="
},
"node_modules/picocolors": {
"version": "1.0.0",
@@ -8414,6 +9288,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -8491,12 +9381,12 @@
"dev": true
},
"node_modules/playwright": {
- "version": "1.41.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz",
- "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
+ "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"dev": true,
"dependencies": {
- "playwright-core": "1.41.1"
+ "playwright-core": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@@ -8509,9 +9399,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.41.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz",
- "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
+ "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -8538,10 +9428,19 @@
"node": ">=12.0.0"
}
},
+ "node_modules/possible-typed-array-names": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/postcss": {
- "version": "8.4.33",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
- "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
@@ -8559,7 +9458,7 @@
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -8580,10 +9479,74 @@
"node": "^12 || >=14"
}
},
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-loader": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz",
+ "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==",
+ "dependencies": {
+ "cosmiconfig": "^9.0.0",
+ "jiti": "^1.20.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "postcss": "^7.0.0 || ^8.0.1",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
"node_modules/postcss-modules-extract-imports": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
- "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+ "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
"engines": {
"node": "^10 || ^12 || >= 14"
},
@@ -8592,9 +9555,9 @@
}
},
"node_modules/postcss-modules-local-by-default": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz",
- "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz",
+ "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==",
"dependencies": {
"icss-utils": "^5.0.0",
"postcss-selector-parser": "^6.0.2",
@@ -8608,9 +9571,9 @@
}
},
"node_modules/postcss-modules-scope": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz",
- "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz",
+ "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==",
"dependencies": {
"postcss-selector-parser": "^6.0.4"
},
@@ -8635,6 +9598,50 @@
"postcss": "^8.1.0"
}
},
+ "node_modules/postcss-nested": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
+ "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.11"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-nesting": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.1.tgz",
+ "integrity": "sha512-qc74KvIAQNa5ujZKG1UV286dhaDW6basbUy2i9AzNU/T8C9hpvGu9NZzm1SfePe2yP7sPYgpA8d4sPVopn2Hhw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "dependencies": {
+ "@csstools/selector-resolve-nested": "^1.1.0",
+ "@csstools/selector-specificity": "^3.0.3",
+ "postcss-selector-parser": "^6.0.13"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"node_modules/postcss-resolve-nested-selector": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
@@ -8684,9 +9691,9 @@
}
},
"node_modules/postcss-selector-parser": {
- "version": "6.0.15",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
- "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
+ "version": "6.0.16",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
+ "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -8728,6 +9735,33 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
+ "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -8783,12 +9817,6 @@
"node": ">=4"
}
},
- "node_modules/psl": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
- "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
- "dev": true
- },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -8806,12 +9834,6 @@
"node": ">=6"
}
},
- "node_modules/querystringify": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
- "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
- "dev": true
- },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -8845,6 +9867,14 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
"node_modules/read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@@ -8947,6 +9977,17 @@
"node": ">=8"
}
},
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
"node_modules/rechoir": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@@ -8970,6 +10011,27 @@
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
+ "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.1",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "globalthis": "^1.0.3",
+ "which-builtin-type": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -8998,14 +10060,15 @@
}
},
"node_modules/regexp.prototype.flags": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
- "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+ "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "set-function-name": "^2.0.0"
+ "call-bind": "^1.0.6",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "set-function-name": "^2.0.1"
},
"engines": {
"node": ">= 0.4"
@@ -9052,12 +10115,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/requires-port": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
- "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
- "dev": true
- },
"node_modules/reserved": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/reserved/-/reserved-0.1.2.tgz",
@@ -9106,7 +10163,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -9163,12 +10219,6 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/rrweb-cssom": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
- "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
- "dev": true
- },
"node_modules/run-con": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz",
@@ -9223,13 +10273,13 @@
}
},
"node_modules/safe-array-concat": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
- "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+ "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.5",
- "get-intrinsic": "^1.2.2",
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4",
"has-symbols": "^1.0.3",
"isarray": "^2.0.5"
},
@@ -9260,13 +10310,13 @@
]
},
"node_modules/safe-regex-test": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
- "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+ "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.5",
- "get-intrinsic": "^1.2.2",
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
"is-regex": "^1.1.4"
},
"engines": {
@@ -9293,18 +10343,6 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
- "node_modules/saxes": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
- "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
- "dev": true,
- "dependencies": {
- "xmlchars": "^2.2.0"
- },
- "engines": {
- "node": ">=v12.22.7"
- }
- },
"node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@@ -9338,9 +10376,9 @@
}
},
"node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -9360,17 +10398,17 @@
}
},
"node_modules/seroval": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.4.tgz",
- "integrity": "sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.5.tgz",
+ "integrity": "sha512-TM+Z11tHHvQVQKeNlOUonOWnsNM+2IBwZ4vwoi4j3zKzIpc5IDw8WPwCfcc8F17wy6cBcJGbZbFOR0UCuTZHQA==",
"engines": {
"node": ">=10"
}
},
"node_modules/seroval-plugins": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.4.tgz",
- "integrity": "sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.5.tgz",
+ "integrity": "sha512-8+pDC1vOedPXjKG7oz8o+iiHrtF2WswaMQJ7CKFpccvSYfrzmvKY9zOJWCg+881722wIHfwkdnRmiiDm9ym+zQ==",
"engines": {
"node": ">=10"
},
@@ -9379,30 +10417,32 @@
}
},
"node_modules/set-function-length": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
- "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"dependencies": {
- "define-data-property": "^1.1.1",
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
"function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.2",
+ "get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.1"
+ "has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/set-function-name": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
- "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"dev": true,
"dependencies": {
- "define-data-property": "^1.0.1",
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
"functions-have-names": "^1.2.3",
- "has-property-descriptors": "^1.0.0"
+ "has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -9439,14 +10479,18 @@
}
},
"node_modules/side-channel": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
- "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.0",
- "get-intrinsic": "^1.0.2",
- "object-inspect": "^1.9.0"
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -9462,7 +10506,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
"engines": {
"node": ">=14"
},
@@ -9509,12 +10552,12 @@
}
},
"node_modules/solid-js": {
- "version": "1.8.12",
- "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.12.tgz",
- "integrity": "sha512-sLE/i6M9FSWlov3a2pTC5ISzanH2aKwqXTZj+bbFt4SUrVb4iGEa7fpILBMOxsQjkv3eXqEk6JVLlogOdTe0UQ==",
+ "version": "1.8.16",
+ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.16.tgz",
+ "integrity": "sha512-rja94MNU9flF3qQRLNsu60QHKBDKBkVE1DldJZPIfn2ypIn3NV2WpSbGTQIvsyGPBo+9E2IMjwqnqpbgfWuzeg==",
"dependencies": {
"csstype": "^3.1.0",
- "seroval": "^1.0.3",
+ "seroval": "^1.0.4",
"seroval-plugins": "^1.0.3"
}
},
@@ -9537,9 +10580,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": {
"node": ">=0.10.0"
}
@@ -9592,9 +10635,9 @@
}
},
"node_modules/spdx-exceptions": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
- "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw=="
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="
},
"node_modules/spdx-expression-parse": {
"version": "3.0.1",
@@ -9614,9 +10657,9 @@
}
},
"node_modules/spdx-license-ids": {
- "version": "3.0.16",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
- "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw=="
+ "version": "3.0.17",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
+ "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg=="
},
"node_modules/spdx-ranges": {
"version": "2.1.1",
@@ -9673,7 +10716,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -9683,15 +10725,26 @@
"node": ">=8"
}
},
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
"node_modules/string.prototype.trim": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
- "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
+ "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.0",
+ "es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -9701,28 +10754,31 @@
}
},
"node_modules/string.prototype.trimend": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
- "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
+ "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.trimstart": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
- "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.2.0",
- "es-abstract": "^1.22.1"
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -9744,7 +10800,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -9752,6 +10807,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -9789,17 +10853,23 @@
}
},
"node_modules/strip-literal": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
- "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz",
+ "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==",
"dev": true,
"dependencies": {
- "acorn": "^8.10.0"
+ "js-tokens": "^9.0.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz",
+ "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==",
+ "dev": true
+ },
"node_modules/style-search": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
@@ -9807,15 +10877,16 @@
"dev": true
},
"node_modules/stylelint": {
- "version": "16.2.1",
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.1.tgz",
- "integrity": "sha512-SfIMGFK+4n7XVAyv50CpVfcGYWG4v41y6xG7PqOgQSY8M/PgdK0SQbjWFblxjJZlN9jNq879mB4BCZHJRIJ1hA==",
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.3.1.tgz",
+ "integrity": "sha512-/JOwQnBvxEKOT2RtNgGpBVXnCSMBgKOL2k7w0K52htwCyJls4+cHvc4YZgXlVoAZS9QJd2DgYAiRnja96pTgxw==",
"dev": true,
"dependencies": {
- "@csstools/css-parser-algorithms": "^2.5.0",
- "@csstools/css-tokenizer": "^2.2.3",
- "@csstools/media-query-list-parser": "^2.1.7",
- "@csstools/selector-specificity": "^3.0.1",
+ "@csstools/css-parser-algorithms": "^2.6.1",
+ "@csstools/css-tokenizer": "^2.2.4",
+ "@csstools/media-query-list-parser": "^2.1.9",
+ "@csstools/selector-specificity": "^3.0.2",
+ "@dual-bundle/import-meta-resolve": "^4.0.0",
"balanced-match": "^2.0.0",
"colord": "^2.9.3",
"cosmiconfig": "^9.0.0",
@@ -9829,19 +10900,19 @@
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.3.1",
- "ignore": "^5.3.0",
+ "ignore": "^5.3.1",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
- "known-css-properties": "^0.29.0",
+ "known-css-properties": "^0.30.0",
"mathml-tag-names": "^2.1.3",
- "meow": "^13.1.0",
+ "meow": "^13.2.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"picocolors": "^1.0.0",
- "postcss": "^8.4.33",
+ "postcss": "^8.4.38",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^7.0.0",
- "postcss-selector-parser": "^6.0.15",
+ "postcss-selector-parser": "^6.0.16",
"postcss-value-parser": "^4.2.0",
"resolve-from": "^5.0.0",
"string-width": "^4.2.3",
@@ -9886,6 +10957,22 @@
"stylelint": ">=7 <=16"
}
},
+ "node_modules/stylelint-value-no-unknown-custom-properties": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/stylelint-value-no-unknown-custom-properties/-/stylelint-value-no-unknown-custom-properties-6.0.1.tgz",
+ "integrity": "sha512-N60PTdaTknB35j6D4FhW0GL2LlBRV++bRpXMMldWMQZ240yFQaoltzlLY4lXXs7Z0J5mNUYZQ/gjyVtU2DhCMA==",
+ "dev": true,
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0",
+ "resolve": "^1.22.8"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": ">=16"
+ }
+ },
"node_modules/stylelint/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@@ -9917,41 +11004,18 @@
}
},
"node_modules/stylelint/node_modules/flat-cache": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.0.tgz",
- "integrity": "sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"dependencies": {
"flatted": "^3.2.9",
- "keyv": "^4.5.4",
- "rimraf": "^5.0.5"
+ "keyv": "^4.5.4"
},
"engines": {
"node": ">=16"
}
},
- "node_modules/stylelint/node_modules/glob": {
- "version": "10.3.10",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
- "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
- "dev": true,
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^2.3.5",
- "minimatch": "^9.0.1",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
- "path-scurry": "^1.10.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/stylelint/node_modules/postcss-safe-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz",
@@ -9987,24 +11051,6 @@
"node": ">=8"
}
},
- "node_modules/stylelint/node_modules/rimraf": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
- "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
- "dev": true,
- "dependencies": {
- "glob": "^10.3.7"
- },
- "bin": {
- "rimraf": "dist/esm/bin.mjs"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/stylelint/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
@@ -10054,6 +11100,56 @@
"node": ">= 8"
}
},
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sucrase/node_modules/glob": {
+ "version": "10.3.12",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+ "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.6",
+ "minimatch": "^9.0.1",
+ "minipass": "^7.0.4",
+ "path-scurry": "^1.10.2"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/superstruct": {
"version": "0.10.13",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.13.tgz",
@@ -10094,6 +11190,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svg-element-attributes": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/svg-element-attributes/-/svg-element-attributes-1.3.1.tgz",
+ "integrity": "sha512-Bh05dSOnJBf3miNMqpsormfNtfidA/GxQVakhtn0T4DECWKeXQRQUceYjJ+OxYiiLdGe4Jo9iFV8wICFapFeIA==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/svg-tags": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
@@ -10135,15 +11241,9 @@
}
},
"node_modules/swagger-ui-dist": {
- "version": "5.11.2",
- "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz",
- "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A=="
- },
- "node_modules/symbol-tree": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
- "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
- "dev": true
+ "version": "5.13.0",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.13.0.tgz",
+ "integrity": "sha512-uaWhh6j18IIs5tOX0arvIBnVINAzpTXaQXkr7qAk8zoupegJVg0UU/5+S/FgsgVCnzVsJ9d7QLjIxkswEeTg0Q=="
},
"node_modules/sync-fetch": {
"version": "0.4.5",
@@ -10157,10 +11257,26 @@
"node": ">=14"
}
},
+ "node_modules/synckit": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz",
+ "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
+ "dev": true,
+ "dependencies": {
+ "@pkgr/core": "^0.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
"node_modules/table": {
- "version": "6.8.1",
- "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
- "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+ "version": "6.8.2",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz",
+ "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==",
"dev": true,
"dependencies": {
"ajv": "^8.0.1",
@@ -10173,6 +11289,87 @@
"node": ">=10.0.0"
}
},
+ "node_modules/tailwindcss": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
+ "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.5.3",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.0",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.0",
+ "lilconfig": "^2.1.0",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.0.0",
+ "postcss": "^8.4.23",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.1",
+ "postcss-nested": "^6.0.1",
+ "postcss-selector-parser": "^6.0.11",
+ "resolve": "^1.22.2",
+ "sucrase": "^3.32.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
+ "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -10181,10 +11378,23 @@
"node": ">=6"
}
},
+ "node_modules/temporal-polyfill": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.2.3.tgz",
+ "integrity": "sha512-7ZJRc7wq/1XjrOQYkkNpgo2qfE9XLrUU8D/DS+LAC/T0bYqZ46rW6dow0sOTXTPZS4bwer8bD/0OyuKQBfA3yw==",
+ "dependencies": {
+ "temporal-spec": "^0.2.0"
+ }
+ },
+ "node_modules/temporal-spec": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.2.0.tgz",
+ "integrity": "sha512-r1AT0XdEp8TMQ13FLvOt8mOtAxDQsRt2QU5rSWCA7YfshddU651Y1NHVrceLANvixKdf9fYO8B/S9fXHodB7HQ=="
+ },
"node_modules/terser": {
- "version": "5.27.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
- "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
+ "version": "5.30.3",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
+ "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@@ -10287,6 +11497,25 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/throttle-debounce": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
@@ -10307,18 +11536,18 @@
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/tinypool": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
- "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.3.tgz",
+ "integrity": "sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tinyspy": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
- "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
+ "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
"dev": true,
"engines": {
"node": ">=14.0.0"
@@ -10348,41 +11577,10 @@
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
"integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ=="
},
- "node_modules/tough-cookie": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
- "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
- "dev": true,
- "dependencies": {
- "psl": "^1.1.33",
- "punycode": "^2.1.1",
- "universalify": "^0.2.0",
- "url-parse": "^1.5.3"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/tough-cookie/node_modules/universalify": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
- "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
- "dev": true,
- "engines": {
- "node": ">= 4.0.0"
- }
- },
"node_modules/tr46": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
- "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
- "dev": true,
- "dependencies": {
- "punycode": "^2.3.1"
- },
- "engines": {
- "node": ">=18"
- }
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tributejs": {
"version": "5.1.3",
@@ -10390,9 +11588,9 @@
"integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ=="
},
"node_modules/ts-api-utils": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz",
- "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
+ "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
"dev": true,
"engines": {
"node": ">=16"
@@ -10409,6 +11607,35 @@
"node": ">=6.10"
}
},
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -10449,29 +11676,30 @@
}
},
"node_modules/typed-array-buffer": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
- "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.2.1",
- "is-typed-array": "^1.1.10"
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/typed-array-byte-length": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
- "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
+ "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
+ "call-bind": "^1.0.7",
"for-each": "^0.3.3",
- "has-proto": "^1.0.1",
- "is-typed-array": "^1.1.10"
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
@@ -10481,16 +11709,17 @@
}
},
"node_modules/typed-array-byte-offset": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
- "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
+ "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
"dev": true,
"dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
"for-each": "^0.3.3",
- "has-proto": "^1.0.1",
- "is-typed-array": "^1.1.10"
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
@@ -10500,23 +11729,29 @@
}
},
"node_modules/typed-array-length": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
- "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
+ "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
"dev": true,
"dependencies": {
- "call-bind": "^1.0.2",
+ "call-bind": "^1.0.7",
"for-each": "^0.3.3",
- "is-typed-array": "^1.1.9"
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typescript": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
- "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
+ "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
"devOptional": true,
"peer": true,
"bin": {
@@ -10528,20 +11763,20 @@
}
},
"node_modules/typo-js": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.3.tgz",
- "integrity": "sha512-67Hyl94beZX8gmTap7IDPrG5hy2cHftgsCAcGvE1tzuxGT+kRB+zSBin0wIMwysYw8RUCBCvv9UfQl8TNM75dA=="
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz",
+ "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA=="
},
"node_modules/uc.micro": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.0.0.tgz",
- "integrity": "sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"dev": true
},
"node_modules/ufo": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz",
- "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==",
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
+ "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==",
"dev": true
},
"node_modules/uint8-to-base64": {
@@ -10620,12 +11855,12 @@
}
},
"node_modules/updates": {
- "version": "15.1.1",
- "resolved": "https://registry.npmjs.org/updates/-/updates-15.1.1.tgz",
- "integrity": "sha512-dMz/4251b0lV7yR58tuydCKaiWxOa18YM8fnRgtiDVzQ5ALopTZhMckv00w0nSMj6OFMFKLshTZGkX4dAebaaw==",
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/updates/-/updates-16.0.0.tgz",
+ "integrity": "sha512-Ra3QUu/rfbSCsG83zNNvoRQt0FVT3qULBSALYTlwTDX6oyz7R5GQAYwqJoIG/RDjYAXpwr3usrInoyHHTP6cag==",
"dev": true,
"bin": {
- "updates": "bin/updates.js"
+ "updates": "dist/updates.js"
},
"engines": {
"node": ">=18"
@@ -10645,16 +11880,6 @@
"integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
"dev": true
},
- "node_modules/url-parse": {
- "version": "1.5.10",
- "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
- "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
- "dev": true,
- "dependencies": {
- "querystringify": "^2.1.1",
- "requires-port": "^1.0.0"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -10717,15 +11942,20 @@
"builtins": "^1.0.3"
}
},
+ "node_modules/vanilla-colorful": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz",
+ "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
+ },
"node_modules/vite": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
- "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
+ "version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
+ "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
"dev": true,
"dependencies": {
- "esbuild": "^0.19.3",
- "postcss": "^8.4.32",
- "rollup": "^4.2.0"
+ "esbuild": "^0.20.1",
+ "postcss": "^8.4.38",
+ "rollup": "^4.13.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -10773,9 +12003,9 @@
}
},
"node_modules/vite-node": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
- "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
+ "integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -10795,9 +12025,9 @@
}
},
"node_modules/vite-string-plugin": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.3.tgz",
- "integrity": "sha512-uHL8BV2tBf32T2slYpS0vRzGVrAS3iuivtGknjzyecvpSq2AiBSkyLAjEvvIZuZGDDGFHyGX+5+yc3OBPjWDlA==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.1.5.tgz",
+ "integrity": "sha512-KRCIFX3PWVUuEjpi9O7EKLT9E27OqOA3RimIvVx6cziLAUxvnk2VvHQfMrP+mKkqyqqSmnnYyTig3OyDnK/zlA==",
"dev": true
},
"node_modules/vite/node_modules/@types/estree": {
@@ -10821,9 +12051,9 @@
}
},
"node_modules/vite/node_modules/rollup": {
- "version": "4.9.6",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz",
- "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz",
+ "integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.5"
@@ -10836,35 +12066,36 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.9.6",
- "@rollup/rollup-android-arm64": "4.9.6",
- "@rollup/rollup-darwin-arm64": "4.9.6",
- "@rollup/rollup-darwin-x64": "4.9.6",
- "@rollup/rollup-linux-arm-gnueabihf": "4.9.6",
- "@rollup/rollup-linux-arm64-gnu": "4.9.6",
- "@rollup/rollup-linux-arm64-musl": "4.9.6",
- "@rollup/rollup-linux-riscv64-gnu": "4.9.6",
- "@rollup/rollup-linux-x64-gnu": "4.9.6",
- "@rollup/rollup-linux-x64-musl": "4.9.6",
- "@rollup/rollup-win32-arm64-msvc": "4.9.6",
- "@rollup/rollup-win32-ia32-msvc": "4.9.6",
- "@rollup/rollup-win32-x64-msvc": "4.9.6",
+ "@rollup/rollup-android-arm-eabi": "4.14.0",
+ "@rollup/rollup-android-arm64": "4.14.0",
+ "@rollup/rollup-darwin-arm64": "4.14.0",
+ "@rollup/rollup-darwin-x64": "4.14.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.14.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.14.0",
+ "@rollup/rollup-linux-arm64-musl": "4.14.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.14.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.14.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.14.0",
+ "@rollup/rollup-linux-x64-gnu": "4.14.0",
+ "@rollup/rollup-linux-x64-musl": "4.14.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.14.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.14.0",
+ "@rollup/rollup-win32-x64-msvc": "4.14.0",
"fsevents": "~2.3.2"
}
},
"node_modules/vitest": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
- "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
+ "integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
"dev": true,
"dependencies": {
- "@vitest/expect": "1.2.2",
- "@vitest/runner": "1.2.2",
- "@vitest/snapshot": "1.2.2",
- "@vitest/spy": "1.2.2",
- "@vitest/utils": "1.2.2",
+ "@vitest/expect": "1.4.0",
+ "@vitest/runner": "1.4.0",
+ "@vitest/snapshot": "1.4.0",
+ "@vitest/spy": "1.4.0",
+ "@vitest/utils": "1.4.0",
"acorn-walk": "^8.3.2",
- "cac": "^6.7.14",
"chai": "^4.3.10",
"debug": "^4.3.4",
"execa": "^8.0.1",
@@ -10873,11 +12104,11 @@
"pathe": "^1.1.1",
"picocolors": "^1.0.0",
"std-env": "^3.5.0",
- "strip-literal": "^1.3.0",
+ "strip-literal": "^2.0.0",
"tinybench": "^2.5.1",
"tinypool": "^0.8.2",
"vite": "^5.0.0",
- "vite-node": "1.2.2",
+ "vite-node": "1.4.0",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -10892,8 +12123,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "^1.0.0",
- "@vitest/ui": "^1.0.0",
+ "@vitest/browser": "1.4.0",
+ "@vitest/ui": "1.4.0",
"happy-dom": "*",
"jsdom": "*"
},
@@ -10919,9 +12150,9 @@
}
},
"node_modules/vitest/node_modules/magic-string": {
- "version": "0.30.7",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
- "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
+ "version": "0.30.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz",
+ "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -10931,15 +12162,15 @@
}
},
"node_modules/vue": {
- "version": "3.4.15",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
- "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
+ "version": "3.4.21",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
+ "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
"dependencies": {
- "@vue/compiler-dom": "3.4.15",
- "@vue/compiler-sfc": "3.4.15",
- "@vue/runtime-dom": "3.4.15",
- "@vue/server-renderer": "3.4.15",
- "@vue/shared": "3.4.15"
+ "@vue/compiler-dom": "3.4.21",
+ "@vue/compiler-sfc": "3.4.21",
+ "@vue/runtime-dom": "3.4.21",
+ "@vue/server-renderer": "3.4.21",
+ "@vue/shared": "3.4.21"
},
"peerDependencies": {
"typescript": "*"
@@ -10959,6 +12190,15 @@
"vue": "^3.2.37"
}
},
+ "node_modules/vue-chartjs": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz",
+ "integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "vue": "^3.0.0-0 || ^2.7.0"
+ }
+ },
"node_modules/vue-eslint-parser": {
"version": "9.4.2",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz",
@@ -11016,31 +12256,10 @@
"vue": "^3.2.29"
}
},
- "node_modules/w3c-xmlserializer": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
- "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
- "dev": true,
- "dependencies": {
- "xml-name-validator": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
- "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
- "dev": true,
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/watchpack": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
- "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
+ "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@@ -11064,25 +12283,25 @@
}
},
"node_modules/webpack": {
- "version": "5.90.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.1.tgz",
- "integrity": "sha512-SstPdlAC5IvgFnhiRok8hqJo/+ArAbNv7rhU4fnWGHNVfN59HSQFaxZDSAL3IFG2YmqxuRs+IU33milSxbPlog==",
+ "version": "5.91.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz",
+ "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
- "@webassemblyjs/ast": "^1.11.5",
- "@webassemblyjs/wasm-edit": "^1.11.5",
- "@webassemblyjs/wasm-parser": "^1.11.5",
+ "@webassemblyjs/ast": "^1.12.1",
+ "@webassemblyjs/wasm-edit": "^1.12.1",
+ "@webassemblyjs/wasm-parser": "^1.12.1",
"acorn": "^8.7.1",
"acorn-import-assertions": "^1.9.0",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.15.0",
+ "enhanced-resolve": "^5.16.0",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.2.9",
+ "graceful-fs": "^4.2.11",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
@@ -11090,7 +12309,7 @@
"schema-utils": "^3.2.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.10",
- "watchpack": "^2.4.0",
+ "watchpack": "^2.4.1",
"webpack-sources": "^3.2.3"
},
"bin": {
@@ -11261,40 +12480,29 @@
"node": ">=10.13.0"
}
},
- "node_modules/whatwg-encoding": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
- "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
- "dev": true,
- "dependencies": {
- "iconv-lite": "0.6.3"
- },
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/whatwg-mimetype": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
- "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true,
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/whatwg-url": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
- "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
- "dev": true,
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
- "tr46": "^5.0.0",
- "webidl-conversions": "^7.0.0"
- },
- "engines": {
- "node": ">=18"
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
}
},
+ "node_modules/whatwg-url/node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -11325,17 +12533,61 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/which-typed-array": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
- "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+ "node_modules/which-builtin-type": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
+ "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
"dev": true,
"dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.4",
+ "function.prototype.name": "^1.1.5",
+ "has-tostringtag": "^1.0.0",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.0.5",
+ "is-finalizationregistry": "^1.0.2",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.1.4",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+ "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
- "has-tostringtag": "^1.0.0"
+ "has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -11386,7 +12638,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -11474,27 +12725,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/ws": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
- "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
- "dev": true,
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
@@ -11504,12 +12734,6 @@
"node": ">=12"
}
},
- "node_modules/xmlchars": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
- "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
- "dev": true
- },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -11524,19 +12748,30 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
+ "node_modules/yaml": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
+ "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/yargs": {
- "version": "17.3.1",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
- "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"dependencies": {
- "cliui": "^7.0.2",
+ "cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
- "yargs-parser": "^21.0.0"
+ "yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
@@ -11551,6 +12786,37 @@
"node": ">=12"
}
},
+ "node_modules/yargs/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 569955d815..f58c3b4d8f 100644
--- a/package.json
+++ b/package.json
@@ -4,89 +4,99 @@
"node": ">= 18.0.0"
},
"dependencies": {
- "@citation-js/core": "0.7.6",
- "@citation-js/plugin-bibtex": "0.7.8",
- "@citation-js/plugin-csl": "0.7.6",
+ "@citation-js/core": "0.7.9",
+ "@citation-js/plugin-bibtex": "0.7.9",
+ "@citation-js/plugin-csl": "0.7.9",
"@citation-js/plugin-software-formats": "0.6.1",
- "@claviska/jquery-minicolors": "2.3.6",
- "@github/markdown-toolbar-element": "2.2.1",
- "@github/relative-time-element": "4.3.1",
+ "@github/markdown-toolbar-element": "2.2.3",
+ "@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
- "@primer/octicons": "19.8.0",
- "@webcomponents/custom-elements": "1.6.0",
+ "@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
- "asciinema-player": "3.6.3",
- "clippie": "4.0.6",
- "css-loader": "6.10.0",
+ "asciinema-player": "3.7.1",
+ "chart.js": "4.4.2",
+ "chartjs-adapter-dayjs-4": "1.0.4",
+ "chartjs-plugin-zoom": "2.0.1",
+ "clippie": "4.0.7",
+ "css-loader": "7.0.0",
+ "dayjs": "1.11.10",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
- "esbuild-loader": "4.0.3",
+ "esbuild-loader": "4.1.0",
"escape-goat": "4.0.0",
"fast-glob": "3.3.2",
- "htmx.org": "1.9.10",
+ "htmx.org": "1.9.11",
+ "idiomorph": "0.3.0",
"jquery": "3.7.1",
- "katex": "0.16.9",
+ "katex": "0.16.10",
"license-checker-webpack-plugin": "0.2.1",
- "mermaid": "10.7.0",
- "mini-css-extract-plugin": "2.8.0",
- "minimatch": "9.0.3",
- "monaco-editor": "0.45.0",
+ "mermaid": "10.9.0",
+ "mini-css-extract-plugin": "2.8.1",
+ "minimatch": "9.0.4",
+ "monaco-editor": "0.47.0",
"monaco-editor-webpack-plugin": "7.1.0",
- "pdfobject": "2.2.12",
+ "pdfobject": "2.3.0",
+ "postcss": "8.4.38",
+ "postcss-loader": "8.1.1",
+ "postcss-nesting": "12.1.1",
"pretty-ms": "9.0.0",
"sortablejs": "1.15.2",
- "swagger-ui-dist": "5.11.2",
+ "swagger-ui-dist": "5.13.0",
+ "tailwindcss": "3.4.3",
+ "temporal-polyfill": "0.2.3",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tippy.js": "6.3.7",
"toastify-js": "1.12.0",
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
- "vue": "3.4.15",
+ "vanilla-colorful": "0.7.2",
+ "vue": "3.4.21",
"vue-bar-graph": "2.0.0",
+ "vue-chartjs": "5.3.0",
"vue-loader": "17.4.2",
"vue3-calendar-heatmap": "2.0.5",
- "webpack": "5.90.1",
+ "webpack": "5.91.0",
"webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
- "@playwright/test": "1.41.1",
- "@stoplight/spectral-cli": "6.11.0",
- "@stylistic/eslint-plugin-js": "1.5.4",
- "@stylistic/stylelint-plugin": "2.0.0",
- "@vitejs/plugin-vue": "5.0.3",
- "eslint": "8.56.0",
+ "@playwright/test": "1.42.1",
+ "@stoplight/spectral-cli": "6.11.1",
+ "@stylistic/eslint-plugin-js": "1.7.0",
+ "@stylistic/stylelint-plugin": "2.1.1",
+ "@vitejs/plugin-vue": "5.0.4",
+ "eslint": "8.57.0",
"eslint-plugin-array-func": "4.0.0",
+ "eslint-plugin-github": "4.10.2",
"eslint-plugin-i": "2.29.1",
"eslint-plugin-jquery": "1.5.1",
"eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-use-extend-native": "0.5.0",
- "eslint-plugin-regexp": "2.2.0",
- "eslint-plugin-sonarjs": "0.23.0",
- "eslint-plugin-unicorn": "50.0.1",
- "eslint-plugin-vitest": "0.3.21",
- "eslint-plugin-vitest-globals": "1.4.0",
- "eslint-plugin-vue": "9.21.1",
- "eslint-plugin-vue-scoped-css": "2.7.2",
+ "eslint-plugin-regexp": "2.4.0",
+ "eslint-plugin-sonarjs": "0.25.1",
+ "eslint-plugin-unicorn": "52.0.0",
+ "eslint-plugin-vitest": "0.4.1",
+ "eslint-plugin-vitest-globals": "1.5.0",
+ "eslint-plugin-vue": "9.24.0",
+ "eslint-plugin-vue-scoped-css": "2.8.0",
"eslint-plugin-wc": "2.0.4",
- "jsdom": "24.0.0",
+ "happy-dom": "14.5.0",
"markdownlint-cli": "0.39.0",
"postcss-html": "1.6.0",
- "stylelint": "16.2.1",
+ "stylelint": "16.3.1",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.4",
+ "stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.2.0",
- "updates": "15.1.1",
- "vite-string-plugin": "1.1.3",
- "vitest": "1.2.2"
+ "updates": "16.0.0",
+ "vite-string-plugin": "1.1.5",
+ "vitest": "1.4.0"
},
"browserslist": [
- "defaults",
- "not ie > 0",
- "not ie_mob > 0"
+ "defaults"
]
}
diff --git a/playwright.config.js b/playwright.config.js
index b7badf1cc0..bdd303ae25 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -20,7 +20,7 @@ export default {
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
- timeout: 2000
+ timeout: 2000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
diff --git a/poetry.lock b/poetry.lock
index 74d202c919..951a0fa7a8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "click"
@@ -27,12 +27,12 @@ files = [
[[package]]
name = "cssbeautifier"
-version = "1.14.11"
+version = "1.15.1"
description = "CSS unobfuscator and beautifier."
optional = false
python-versions = "*"
files = [
- {file = "cssbeautifier-1.14.11.tar.gz", hash = "sha256:40544c2b62bbcb64caa5e7f37a02df95654e5ce1bcacadac4ca1f3dc89c31513"},
+ {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"},
]
[package.dependencies]
@@ -67,13 +67,12 @@ tqdm = ">=4.62.2,<5.0.0"
[[package]]
name = "editorconfig"
-version = "0.12.3"
+version = "0.12.4"
description = "EditorConfig File Locator and Interpreter for Python"
optional = false
python-versions = "*"
files = [
- {file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"},
- {file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
+ {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"},
]
[[package]]
@@ -100,12 +99,12 @@ files = [
[[package]]
name = "jsbeautifier"
-version = "1.14.11"
+version = "1.15.1"
description = "JavaScript unobfuscator and beautifier."
optional = false
python-versions = "*"
files = [
- {file = "jsbeautifier-1.14.11.tar.gz", hash = "sha256:6b632581ea60dd1c133cd25a48ad187b4b91f526623c4b0fb5443ef805250505"},
+ {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"},
]
[package.dependencies]
@@ -114,18 +113,15 @@ six = ">=1.13.0"
[[package]]
name = "json5"
-version = "0.9.14"
+version = "0.9.24"
description = "A Python implementation of the JSON5 data format."
optional = false
-python-versions = "*"
+python-versions = ">=3.8"
files = [
- {file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
- {file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
+ {file = "json5-0.9.24-py3-none-any.whl", hash = "sha256:4ca101fd5c7cb47960c055ef8f4d0e31e15a7c6c48c3b6f1473fc83b6c462a13"},
+ {file = "json5-0.9.24.tar.gz", hash = "sha256:0c638399421da959a20952782800e5c1a78c14e08e1dc9738fa10d8ec14d58c8"},
]
-[package.extras]
-dev = ["hypothesis"]
-
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -322,13 +318,13 @@ files = [
[[package]]
name = "tqdm"
-version = "4.66.1"
+version = "4.66.2"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
files = [
- {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"},
- {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"},
+ {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"},
+ {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"},
]
[package.dependencies]
@@ -342,13 +338,13 @@ telegram = ["requests"]
[[package]]
name = "yamllint"
-version = "1.33.0"
+version = "1.35.1"
description = "A linter for YAML files."
optional = false
python-versions = ">=3.8"
files = [
- {file = "yamllint-1.33.0-py3-none-any.whl", hash = "sha256:28a19f5d68d28d8fec538a1db21bb2d84c7dc2e2ea36266da8d4d1c5a683814d"},
- {file = "yamllint-1.33.0.tar.gz", hash = "sha256:2dceab9ef2d99518a2fcf4ffc964d44250ac4459be1ba3ca315118e4a1a81f7d"},
+ {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
+ {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
]
[package.dependencies]
@@ -360,5 +356,5 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata]
lock-version = "2.0"
-python-versions = "^3.8"
-content-hash = "175c87d138a47ba190a2c3f16b801f694915cc6f2367a358585df9cd1b17ff96"
+python-versions = "^3.10"
+content-hash = "cd2ff218e9f27a464dfbc8ec2387824a90f4360e04c3f2e58cc375796b7df33a"
diff --git a/public/assets/img/favicon.svg b/public/assets/img/favicon.svg
index afeeacb77c..43291345df 100644
--- a/public/assets/img/favicon.svg
+++ b/public/assets/img/favicon.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/assets/img/logo.svg b/public/assets/img/logo.svg
index afeeacb77c..43291345df 100644
--- a/public/assets/img/logo.svg
+++ b/public/assets/img/logo.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-bitbucket.svg b/public/assets/img/svg/gitea-bitbucket.svg
index b900335ea1..83e4c5c6e7 100644
--- a/public/assets/img/svg/gitea-bitbucket.svg
+++ b/public/assets/img/svg/gitea-bitbucket.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-discord.svg b/public/assets/img/svg/gitea-discord.svg
index 6ebbdcdcc3..2edcb4fed7 100644
--- a/public/assets/img/svg/gitea-discord.svg
+++ b/public/assets/img/svg/gitea-discord.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-facebook.svg b/public/assets/img/svg/gitea-facebook.svg
index cbeb76b127..6101becad2 100644
--- a/public/assets/img/svg/gitea-facebook.svg
+++ b/public/assets/img/svg/gitea-facebook.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-jetbrains.svg b/public/assets/img/svg/gitea-jetbrains.svg
new file mode 100644
index 0000000000..5821736225
--- /dev/null
+++ b/public/assets/img/svg/gitea-jetbrains.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-microsoftonline.svg b/public/assets/img/svg/gitea-microsoftonline.svg
index ce4f1a5c8f..f2ce13ac22 100644
--- a/public/assets/img/svg/gitea-microsoftonline.svg
+++ b/public/assets/img/svg/gitea-microsoftonline.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-twitter.svg b/public/assets/img/svg/gitea-twitter.svg
index 5d11c6eaec..5ed1e264ca 100644
--- a/public/assets/img/svg/gitea-twitter.svg
+++ b/public/assets/img/svg/gitea-twitter.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/assets/img/svg/gitea-vscodium.svg b/public/assets/img/svg/gitea-vscodium.svg
new file mode 100644
index 0000000000..6aad3d3a64
--- /dev/null
+++ b/public/assets/img/svg/gitea-vscodium.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index d999a1476c..bb768d5cb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,11 +5,11 @@ description = ""
authors = []
[tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.10"
[tool.poetry.group.dev.dependencies]
djlint = "1.34.1"
-yamllint = "1.33.0"
+yamllint = "1.35.1"
[tool.djlint]
profile="golang"
diff --git a/routers/api/actions/artifact.pb.go b/routers/api/actions/artifact.pb.go
new file mode 100644
index 0000000000..590eda9fb9
--- /dev/null
+++ b/routers/api/actions/artifact.pb.go
@@ -0,0 +1,1058 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.32.0
+// protoc v4.25.2
+// source: artifact.proto
+
+package actions
+
+import (
+ reflect "reflect"
+ sync "sync"
+
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type CreateArtifactRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+ WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+ ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
+ Version int32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"`
+}
+
+func (x *CreateArtifactRequest) Reset() {
+ *x = CreateArtifactRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateArtifactRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateArtifactRequest) ProtoMessage() {}
+
+func (x *CreateArtifactRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateArtifactRequest.ProtoReflect.Descriptor instead.
+func (*CreateArtifactRequest) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CreateArtifactRequest) GetWorkflowRunBackendId() string {
+ if x != nil {
+ return x.WorkflowRunBackendId
+ }
+ return ""
+}
+
+func (x *CreateArtifactRequest) GetWorkflowJobRunBackendId() string {
+ if x != nil {
+ return x.WorkflowJobRunBackendId
+ }
+ return ""
+}
+
+func (x *CreateArtifactRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *CreateArtifactRequest) GetExpiresAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.ExpiresAt
+ }
+ return nil
+}
+
+func (x *CreateArtifactRequest) GetVersion() int32 {
+ if x != nil {
+ return x.Version
+ }
+ return 0
+}
+
+type CreateArtifactResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+ SignedUploadUrl string `protobuf:"bytes,2,opt,name=signed_upload_url,json=signedUploadUrl,proto3" json:"signed_upload_url,omitempty"`
+}
+
+func (x *CreateArtifactResponse) Reset() {
+ *x = CreateArtifactResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CreateArtifactResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateArtifactResponse) ProtoMessage() {}
+
+func (x *CreateArtifactResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CreateArtifactResponse.ProtoReflect.Descriptor instead.
+func (*CreateArtifactResponse) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CreateArtifactResponse) GetOk() bool {
+ if x != nil {
+ return x.Ok
+ }
+ return false
+}
+
+func (x *CreateArtifactResponse) GetSignedUploadUrl() string {
+ if x != nil {
+ return x.SignedUploadUrl
+ }
+ return ""
+}
+
+type FinalizeArtifactRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+ WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+ Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
+ Hash *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"`
+}
+
+func (x *FinalizeArtifactRequest) Reset() {
+ *x = FinalizeArtifactRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FinalizeArtifactRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FinalizeArtifactRequest) ProtoMessage() {}
+
+func (x *FinalizeArtifactRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FinalizeArtifactRequest.ProtoReflect.Descriptor instead.
+func (*FinalizeArtifactRequest) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *FinalizeArtifactRequest) GetWorkflowRunBackendId() string {
+ if x != nil {
+ return x.WorkflowRunBackendId
+ }
+ return ""
+}
+
+func (x *FinalizeArtifactRequest) GetWorkflowJobRunBackendId() string {
+ if x != nil {
+ return x.WorkflowJobRunBackendId
+ }
+ return ""
+}
+
+func (x *FinalizeArtifactRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *FinalizeArtifactRequest) GetSize() int64 {
+ if x != nil {
+ return x.Size
+ }
+ return 0
+}
+
+func (x *FinalizeArtifactRequest) GetHash() *wrapperspb.StringValue {
+ if x != nil {
+ return x.Hash
+ }
+ return nil
+}
+
+type FinalizeArtifactResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+ ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
+}
+
+func (x *FinalizeArtifactResponse) Reset() {
+ *x = FinalizeArtifactResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *FinalizeArtifactResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FinalizeArtifactResponse) ProtoMessage() {}
+
+func (x *FinalizeArtifactResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FinalizeArtifactResponse.ProtoReflect.Descriptor instead.
+func (*FinalizeArtifactResponse) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *FinalizeArtifactResponse) GetOk() bool {
+ if x != nil {
+ return x.Ok
+ }
+ return false
+}
+
+func (x *FinalizeArtifactResponse) GetArtifactId() int64 {
+ if x != nil {
+ return x.ArtifactId
+ }
+ return 0
+}
+
+type ListArtifactsRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+ WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+ NameFilter *wrapperspb.StringValue `protobuf:"bytes,3,opt,name=name_filter,json=nameFilter,proto3" json:"name_filter,omitempty"`
+ IdFilter *wrapperspb.Int64Value `protobuf:"bytes,4,opt,name=id_filter,json=idFilter,proto3" json:"id_filter,omitempty"`
+}
+
+func (x *ListArtifactsRequest) Reset() {
+ *x = ListArtifactsRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ListArtifactsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsRequest) ProtoMessage() {}
+
+func (x *ListArtifactsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[4]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsRequest.ProtoReflect.Descriptor instead.
+func (*ListArtifactsRequest) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *ListArtifactsRequest) GetWorkflowRunBackendId() string {
+ if x != nil {
+ return x.WorkflowRunBackendId
+ }
+ return ""
+}
+
+func (x *ListArtifactsRequest) GetWorkflowJobRunBackendId() string {
+ if x != nil {
+ return x.WorkflowJobRunBackendId
+ }
+ return ""
+}
+
+func (x *ListArtifactsRequest) GetNameFilter() *wrapperspb.StringValue {
+ if x != nil {
+ return x.NameFilter
+ }
+ return nil
+}
+
+func (x *ListArtifactsRequest) GetIdFilter() *wrapperspb.Int64Value {
+ if x != nil {
+ return x.IdFilter
+ }
+ return nil
+}
+
+type ListArtifactsResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Artifacts []*ListArtifactsResponse_MonolithArtifact `protobuf:"bytes,1,rep,name=artifacts,proto3" json:"artifacts,omitempty"`
+}
+
+func (x *ListArtifactsResponse) Reset() {
+ *x = ListArtifactsResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ListArtifactsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsResponse) ProtoMessage() {}
+
+func (x *ListArtifactsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[5]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsResponse.ProtoReflect.Descriptor instead.
+func (*ListArtifactsResponse) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *ListArtifactsResponse) GetArtifacts() []*ListArtifactsResponse_MonolithArtifact {
+ if x != nil {
+ return x.Artifacts
+ }
+ return nil
+}
+
+type ListArtifactsResponse_MonolithArtifact struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+ WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+ DatabaseId int64 `protobuf:"varint,3,opt,name=database_id,json=databaseId,proto3" json:"database_id,omitempty"`
+ Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
+ Size int64 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) Reset() {
+ *x = ListArtifactsResponse_MonolithArtifact{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListArtifactsResponse_MonolithArtifact) ProtoMessage() {}
+
+func (x *ListArtifactsResponse_MonolithArtifact) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[6]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListArtifactsResponse_MonolithArtifact.ProtoReflect.Descriptor instead.
+func (*ListArtifactsResponse_MonolithArtifact) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowRunBackendId() string {
+ if x != nil {
+ return x.WorkflowRunBackendId
+ }
+ return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetWorkflowJobRunBackendId() string {
+ if x != nil {
+ return x.WorkflowJobRunBackendId
+ }
+ return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetDatabaseId() int64 {
+ if x != nil {
+ return x.DatabaseId
+ }
+ return 0
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetSize() int64 {
+ if x != nil {
+ return x.Size
+ }
+ return 0
+}
+
+func (x *ListArtifactsResponse_MonolithArtifact) GetCreatedAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return nil
+}
+
+type GetSignedArtifactURLRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+ WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *GetSignedArtifactURLRequest) Reset() {
+ *x = GetSignedArtifactURLRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetSignedArtifactURLRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSignedArtifactURLRequest) ProtoMessage() {}
+
+func (x *GetSignedArtifactURLRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[7]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSignedArtifactURLRequest.ProtoReflect.Descriptor instead.
+func (*GetSignedArtifactURLRequest) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetSignedArtifactURLRequest) GetWorkflowRunBackendId() string {
+ if x != nil {
+ return x.WorkflowRunBackendId
+ }
+ return ""
+}
+
+func (x *GetSignedArtifactURLRequest) GetWorkflowJobRunBackendId() string {
+ if x != nil {
+ return x.WorkflowJobRunBackendId
+ }
+ return ""
+}
+
+func (x *GetSignedArtifactURLRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+type GetSignedArtifactURLResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ SignedUrl string `protobuf:"bytes,1,opt,name=signed_url,json=signedUrl,proto3" json:"signed_url,omitempty"`
+}
+
+func (x *GetSignedArtifactURLResponse) Reset() {
+ *x = GetSignedArtifactURLResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetSignedArtifactURLResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetSignedArtifactURLResponse) ProtoMessage() {}
+
+func (x *GetSignedArtifactURLResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[8]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetSignedArtifactURLResponse.ProtoReflect.Descriptor instead.
+func (*GetSignedArtifactURLResponse) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *GetSignedArtifactURLResponse) GetSignedUrl() string {
+ if x != nil {
+ return x.SignedUrl
+ }
+ return ""
+}
+
+type DeleteArtifactRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ WorkflowRunBackendId string `protobuf:"bytes,1,opt,name=workflow_run_backend_id,json=workflowRunBackendId,proto3" json:"workflow_run_backend_id,omitempty"`
+ WorkflowJobRunBackendId string `protobuf:"bytes,2,opt,name=workflow_job_run_backend_id,json=workflowJobRunBackendId,proto3" json:"workflow_job_run_backend_id,omitempty"`
+ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *DeleteArtifactRequest) Reset() {
+ *x = DeleteArtifactRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *DeleteArtifactRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteArtifactRequest) ProtoMessage() {}
+
+func (x *DeleteArtifactRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[9]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteArtifactRequest.ProtoReflect.Descriptor instead.
+func (*DeleteArtifactRequest) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *DeleteArtifactRequest) GetWorkflowRunBackendId() string {
+ if x != nil {
+ return x.WorkflowRunBackendId
+ }
+ return ""
+}
+
+func (x *DeleteArtifactRequest) GetWorkflowJobRunBackendId() string {
+ if x != nil {
+ return x.WorkflowJobRunBackendId
+ }
+ return ""
+}
+
+func (x *DeleteArtifactRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+type DeleteArtifactResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"`
+ ArtifactId int64 `protobuf:"varint,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"`
+}
+
+func (x *DeleteArtifactResponse) Reset() {
+ *x = DeleteArtifactResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_artifact_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *DeleteArtifactResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeleteArtifactResponse) ProtoMessage() {}
+
+func (x *DeleteArtifactResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_artifact_proto_msgTypes[10]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use DeleteArtifactResponse.ProtoReflect.Descriptor instead.
+func (*DeleteArtifactResponse) Descriptor() ([]byte, []int) {
+ return file_artifact_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *DeleteArtifactResponse) GetOk() bool {
+ if x != nil {
+ return x.Ok
+ }
+ return false
+}
+
+func (x *DeleteArtifactResponse) GetArtifactId() int64 {
+ if x != nil {
+ return x.ArtifactId
+ }
+ return 0
+}
+
+var File_artifact_proto protoreflect.FileDescriptor
+
+var file_artifact_proto_rawDesc = []byte{
+ 0x0a, 0x0e, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x12, 0x1d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+ 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a,
+ 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
+ 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+ 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x22, 0xf5, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
+ 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
+ 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+ 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
+ 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
+ 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
+ 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
+ 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
+ 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+ 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61,
+ 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+ 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
+ 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18,
+ 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52,
+ 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61,
+ 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02,
+ 0x6f, 0x6b, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x75, 0x70, 0x6c,
+ 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73,
+ 0x69, 0x67, 0x6e, 0x65, 0x64, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xe8,
+ 0x01, 0x0a, 0x17, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66,
+ 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f,
+ 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+ 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72,
+ 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49,
+ 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f,
+ 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77,
+ 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12,
+ 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+ 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18,
+ 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61,
+ 0x6c, 0x75, 0x65, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x4b, 0x0a, 0x18, 0x46, 0x69, 0x6e,
+ 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63,
+ 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69,
+ 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, 0x22, 0x84, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41,
+ 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+ 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
+ 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+ 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+ 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+ 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
+ 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
+ 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3d, 0x0a, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x66, 0x69, 0x6c,
+ 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+ 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
+ 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c,
+ 0x74, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+ 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x56, 0x61,
+ 0x6c, 0x75, 0x65, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x7c, 0x0a,
+ 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61,
+ 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x45, 0x2e, 0x67, 0x69, 0x74, 0x68,
+ 0x75, 0x62, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x72, 0x65, 0x73, 0x75, 0x6c,
+ 0x74, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72,
+ 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f,
+ 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
+ 0x52, 0x09, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x26,
+ 0x4c, 0x69, 0x73, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x4d, 0x6f, 0x6e, 0x6f, 0x6c, 0x69, 0x74, 0x68, 0x41, 0x72,
+ 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+ 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69,
+ 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f,
+ 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a,
+ 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75,
+ 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52,
+ 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
+ 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04,
+ 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
+ 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
+ 0x73, 0x69, 0x7a, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f,
+ 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+ 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73,
+ 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22,
+ 0xa6, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74,
+ 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
+ 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75, 0x6e, 0x5f,
+ 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+ 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c,
+ 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65,
+ 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77, 0x6f, 0x72,
+ 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63, 0x6b, 0x65,
+ 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53,
+ 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x55, 0x52, 0x4c,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e,
+ 0x65, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x69,
+ 0x67, 0x6e, 0x65, 0x64, 0x55, 0x72, 0x6c, 0x22, 0xa0, 0x01, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65,
+ 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x35, 0x0a, 0x17, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x72, 0x75,
+ 0x6e, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x75, 0x6e, 0x42,
+ 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x3c, 0x0a, 0x1b, 0x77, 0x6f, 0x72, 0x6b,
+ 0x66, 0x6c, 0x6f, 0x77, 0x5f, 0x6a, 0x6f, 0x62, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x62, 0x61, 0x63,
+ 0x6b, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x77,
+ 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x4a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x42, 0x61, 0x63,
+ 0x6b, 0x65, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x44, 0x65,
+ 0x6c, 0x65, 0x74, 0x65, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
+ 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74,
+ 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66,
+ 0x61, 0x63, 0x74, 0x49, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_artifact_proto_rawDescOnce sync.Once
+ file_artifact_proto_rawDescData = file_artifact_proto_rawDesc
+)
+
+func file_artifact_proto_rawDescGZIP() []byte {
+ file_artifact_proto_rawDescOnce.Do(func() {
+ file_artifact_proto_rawDescData = protoimpl.X.CompressGZIP(file_artifact_proto_rawDescData)
+ })
+ return file_artifact_proto_rawDescData
+}
+
+var (
+ file_artifact_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+ file_artifact_proto_goTypes = []interface{}{
+ (*CreateArtifactRequest)(nil), // 0: github.actions.results.api.v1.CreateArtifactRequest
+ (*CreateArtifactResponse)(nil), // 1: github.actions.results.api.v1.CreateArtifactResponse
+ (*FinalizeArtifactRequest)(nil), // 2: github.actions.results.api.v1.FinalizeArtifactRequest
+ (*FinalizeArtifactResponse)(nil), // 3: github.actions.results.api.v1.FinalizeArtifactResponse
+ (*ListArtifactsRequest)(nil), // 4: github.actions.results.api.v1.ListArtifactsRequest
+ (*ListArtifactsResponse)(nil), // 5: github.actions.results.api.v1.ListArtifactsResponse
+ (*ListArtifactsResponse_MonolithArtifact)(nil), // 6: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
+ (*GetSignedArtifactURLRequest)(nil), // 7: github.actions.results.api.v1.GetSignedArtifactURLRequest
+ (*GetSignedArtifactURLResponse)(nil), // 8: github.actions.results.api.v1.GetSignedArtifactURLResponse
+ (*DeleteArtifactRequest)(nil), // 9: github.actions.results.api.v1.DeleteArtifactRequest
+ (*DeleteArtifactResponse)(nil), // 10: github.actions.results.api.v1.DeleteArtifactResponse
+ (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
+ (*wrapperspb.StringValue)(nil), // 12: google.protobuf.StringValue
+ (*wrapperspb.Int64Value)(nil), // 13: google.protobuf.Int64Value
+ }
+)
+
+var file_artifact_proto_depIdxs = []int32{
+ 11, // 0: github.actions.results.api.v1.CreateArtifactRequest.expires_at:type_name -> google.protobuf.Timestamp
+ 12, // 1: github.actions.results.api.v1.FinalizeArtifactRequest.hash:type_name -> google.protobuf.StringValue
+ 12, // 2: github.actions.results.api.v1.ListArtifactsRequest.name_filter:type_name -> google.protobuf.StringValue
+ 13, // 3: github.actions.results.api.v1.ListArtifactsRequest.id_filter:type_name -> google.protobuf.Int64Value
+ 6, // 4: github.actions.results.api.v1.ListArtifactsResponse.artifacts:type_name -> github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact
+ 11, // 5: github.actions.results.api.v1.ListArtifactsResponse_MonolithArtifact.created_at:type_name -> google.protobuf.Timestamp
+ 6, // [6:6] is the sub-list for method output_type
+ 6, // [6:6] is the sub-list for method input_type
+ 6, // [6:6] is the sub-list for extension type_name
+ 6, // [6:6] is the sub-list for extension extendee
+ 0, // [0:6] is the sub-list for field type_name
+}
+
+func init() { file_artifact_proto_init() }
+func file_artifact_proto_init() {
+ if File_artifact_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_artifact_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateArtifactRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CreateArtifactResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FinalizeArtifactRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*FinalizeArtifactResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ListArtifactsRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ListArtifactsResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*ListArtifactsResponse_MonolithArtifact); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetSignedArtifactURLRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetSignedArtifactURLResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*DeleteArtifactRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_artifact_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*DeleteArtifactResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_artifact_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 11,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_artifact_proto_goTypes,
+ DependencyIndexes: file_artifact_proto_depIdxs,
+ MessageInfos: file_artifact_proto_msgTypes,
+ }.Build()
+ File_artifact_proto = out.File
+ file_artifact_proto_rawDesc = nil
+ file_artifact_proto_goTypes = nil
+ file_artifact_proto_depIdxs = nil
+}
diff --git a/routers/api/actions/artifact.proto b/routers/api/actions/artifact.proto
new file mode 100644
index 0000000000..c68e5d030d
--- /dev/null
+++ b/routers/api/actions/artifact.proto
@@ -0,0 +1,73 @@
+syntax = "proto3";
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/wrappers.proto";
+
+package github.actions.results.api.v1;
+
+message CreateArtifactRequest {
+ string workflow_run_backend_id = 1;
+ string workflow_job_run_backend_id = 2;
+ string name = 3;
+ google.protobuf.Timestamp expires_at = 4;
+ int32 version = 5;
+}
+
+message CreateArtifactResponse {
+ bool ok = 1;
+ string signed_upload_url = 2;
+}
+
+message FinalizeArtifactRequest {
+ string workflow_run_backend_id = 1;
+ string workflow_job_run_backend_id = 2;
+ string name = 3;
+ int64 size = 4;
+ google.protobuf.StringValue hash = 5;
+}
+
+message FinalizeArtifactResponse {
+ bool ok = 1;
+ int64 artifact_id = 2;
+}
+
+message ListArtifactsRequest {
+ string workflow_run_backend_id = 1;
+ string workflow_job_run_backend_id = 2;
+ google.protobuf.StringValue name_filter = 3;
+ google.protobuf.Int64Value id_filter = 4;
+}
+
+message ListArtifactsResponse {
+ repeated ListArtifactsResponse_MonolithArtifact artifacts = 1;
+}
+
+message ListArtifactsResponse_MonolithArtifact {
+ string workflow_run_backend_id = 1;
+ string workflow_job_run_backend_id = 2;
+ int64 database_id = 3;
+ string name = 4;
+ int64 size = 5;
+ google.protobuf.Timestamp created_at = 6;
+}
+
+message GetSignedArtifactURLRequest {
+ string workflow_run_backend_id = 1;
+ string workflow_job_run_backend_id = 2;
+ string name = 3;
+}
+
+message GetSignedArtifactURLResponse {
+ string signed_url = 1;
+}
+
+message DeleteArtifactRequest {
+ string workflow_run_backend_id = 1;
+ string workflow_job_run_backend_id = 2;
+ string name = 3;
+}
+
+message DeleteArtifactResponse {
+ bool ok = 1;
+ int64 artifact_id = 2;
+}
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go
index 3363c4c0e8..d530e9cee5 100644
--- a/routers/api/actions/artifacts.go
+++ b/routers/api/actions/artifacts.go
@@ -63,6 +63,7 @@ package actions
import (
"crypto/md5"
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -70,7 +71,6 @@ import (
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -79,6 +79,7 @@ import (
"code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types"
actions_service "code.gitea.io/gitea/services/actions"
+ "code.gitea.io/gitea/services/context"
)
const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
@@ -426,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
var items []downloadArtifactResponseItem
for _, artifact := range artifacts {
- downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
+ var downloadURL string
+ if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+ u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
+ if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
+ log.Error("Error getting serve direct url: %v", err)
+ }
+ if u != nil {
+ downloadURL = u.String()
+ }
+ }
+ if downloadURL == "" {
+ downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
+ }
item := downloadArtifactResponseItem{
Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),
ItemType: "file",
diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go
index 0713c8bba8..3a81724b3a 100644
--- a/routers/api/actions/artifacts_chunks.go
+++ b/routers/api/actions/artifacts_chunks.go
@@ -5,11 +5,16 @@ package actions
import (
"crypto/md5"
+ "crypto/sha256"
"encoding/base64"
+ "encoding/hex"
+ "errors"
"fmt"
+ "hash"
"io"
"path/filepath"
"sort"
+ "strings"
"time"
"code.gitea.io/gitea/models/actions"
@@ -18,6 +23,52 @@ import (
"code.gitea.io/gitea/modules/storage"
)
+func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
+ artifact *actions.ActionArtifact,
+ contentSize, runID, start, end, length int64, checkMd5 bool,
+) (int64, error) {
+ // build chunk store path
+ storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
+ var r io.Reader = ctx.Req.Body
+ var hasher hash.Hash
+ if checkMd5 {
+ // use io.TeeReader to avoid reading all body to md5 sum.
+ // it writes data to hasher after reading end
+ // if hash is not matched, delete the read-end result
+ hasher = md5.New()
+ r = io.TeeReader(r, hasher)
+ }
+ // save chunk to storage
+ writtenSize, err := st.Save(storagePath, r, -1)
+ if err != nil {
+ return -1, fmt.Errorf("save chunk to storage error: %v", err)
+ }
+ var checkErr error
+ if checkMd5 {
+ // check md5
+ reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
+ chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
+ log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
+ // if md5 not match, delete the chunk
+ if reqMd5String != chunkMd5String {
+ checkErr = fmt.Errorf("md5 not match")
+ }
+ }
+ if writtenSize != contentSize {
+ checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size"))
+ }
+ if checkErr != nil {
+ if err := st.Delete(storagePath); err != nil {
+ log.Error("Error deleting chunk: %s, %v", storagePath, err)
+ }
+ return -1, checkErr
+ }
+ log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
+ storagePath, contentSize, artifact.ID, start, end)
+ // return chunk total size
+ return length, nil
+}
+
func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
artifact *actions.ActionArtifact,
contentSize, runID int64,
@@ -29,33 +80,15 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
return -1, fmt.Errorf("parse content range error: %v", err)
}
- // build chunk store path
- storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
- // use io.TeeReader to avoid reading all body to md5 sum.
- // it writes data to hasher after reading end
- // if hash is not matched, delete the read-end result
- hasher := md5.New()
- r := io.TeeReader(ctx.Req.Body, hasher)
- // save chunk to storage
- writtenSize, err := st.Save(storagePath, r, -1)
- if err != nil {
- return -1, fmt.Errorf("save chunk to storage error: %v", err)
- }
- // check md5
- reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
- chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
- log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
- // if md5 not match, delete the chunk
- if reqMd5String != chunkMd5String || writtenSize != contentSize {
- if err := st.Delete(storagePath); err != nil {
- log.Error("Error deleting chunk: %s, %v", storagePath, err)
- }
- return -1, fmt.Errorf("md5 not match")
- }
- log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
- storagePath, contentSize, artifact.ID, start, end)
- // return chunk total size
- return length, nil
+ return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
+}
+
+func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
+ artifact *actions.ActionArtifact,
+ start, contentSize, runID int64,
+) (int64, error) {
+ end := start + contentSize - 1
+ return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
}
type chunkFileItem struct {
@@ -111,14 +144,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
log.Debug("artifact %d chunks not found", art.ID)
continue
}
- if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil {
+ if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil {
return err
}
}
return nil
}
-func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error {
+func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error {
sort.Slice(chunks, func(i, j int) bool {
return chunks[i].Start < chunks[j].Start
})
@@ -157,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
readers = append(readers, readCloser)
}
mergedReader := io.MultiReader(readers...)
+ shaPrefix := "sha256:"
+ var hash hash.Hash
+ if strings.HasPrefix(checksum, shaPrefix) {
+ hash = sha256.New()
+ }
+ if hash != nil {
+ mergedReader = io.TeeReader(mergedReader, hash)
+ }
// if chunk is gzip, use gz as extension
// download-artifact action will use content-encoding header to decide if it should decompress the file
@@ -185,6 +226,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
}
}()
+ if hash != nil {
+ rawChecksum := hash.Sum(nil)
+ actualChecksum := hex.EncodeToString(rawChecksum)
+ if !strings.HasSuffix(checksum, actualChecksum) {
+ return fmt.Errorf("update artifact error checksum is invalid")
+ }
+ }
+
// save storage path to artifact
log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath)
// if artifact is already uploaded, delete the old file
diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go
index 381e7eb16e..aaf89ef40e 100644
--- a/routers/api/actions/artifacts_utils.go
+++ b/routers/api/actions/artifacts_utils.go
@@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
return task, runID, true
}
+func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
+ task := ctx.ActionTask
+ runID, err := strconv.ParseInt(rawRunID, 10, 64)
+ if err != nil || task.Job.RunID != runID {
+ log.Error("Error runID not match")
+ ctx.Error(http.StatusBadRequest, "run-id does not match")
+ return nil, 0, false
+ }
+ return task, runID, true
+}
+
func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
paramHash := ctx.Params("artifact_hash")
// use artifact name to create upload url
diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go
new file mode 100644
index 0000000000..8300989c75
--- /dev/null
+++ b/routers/api/actions/artifactsv4.go
@@ -0,0 +1,512 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+// GitHub Actions Artifacts V4 API Simple Description
+//
+// 1. Upload artifact
+// 1.1. CreateArtifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact
+// Request:
+// {
+// "workflow_run_backend_id": "21",
+// "workflow_job_run_backend_id": "49",
+// "name": "test",
+// "version": 4
+// }
+// Response:
+// {
+// "ok": true,
+// "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75"
+// }
+// 1.2. Upload Zip Content to Blobstorage (unauthenticated request)
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block
+// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
+// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now
+// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
+// 1.5. FinalizeArtifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact
+// Request
+// {
+// "workflow_run_backend_id": "21",
+// "workflow_job_run_backend_id": "49",
+// "name": "test",
+// "size": "2097",
+// "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4"
+// }
+// Response
+// {
+// "ok": true,
+// "artifactId": "4"
+// }
+// 2. Download artifact
+// 2.1. ListArtifacts and optionally filter by artifact exact name or id
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts
+// Request
+// {
+// "workflow_run_backend_id": "21",
+// "workflow_job_run_backend_id": "49",
+// "name_filter": "test"
+// }
+// Response
+// {
+// "artifacts": [
+// {
+// "workflowRunBackendId": "21",
+// "workflowJobRunBackendId": "49",
+// "databaseId": "4",
+// "name": "test",
+// "size": "2093",
+// "createdAt": "2024-01-23T00:13:28Z"
+// }
+// ]
+// }
+// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact
+// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL
+// Request
+// {
+// "workflow_run_backend_id": "21",
+// "workflow_job_run_backend_id": "49",
+// "name": "test"
+// }
+// Response
+// {
+// "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76"
+// }
+// 2.3. Download Zip from Blobstorage (unauthenticated request)
+// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+
+ "google.golang.org/protobuf/encoding/protojson"
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+const (
+ ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService"
+ ArtifactV4ContentEncoding = "application/zip"
+)
+
+type artifactV4Routes struct {
+ prefix string
+ fs storage.ObjectStorage
+}
+
+func ArtifactV4Contexter() func(next http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ base, baseCleanUp := context.NewBaseContext(resp, req)
+ defer baseCleanUp()
+
+ ctx := &ArtifactContext{Base: base}
+ ctx.AppendContextValue(artifactContextKey, ctx)
+
+ next.ServeHTTP(ctx.Resp, ctx.Req)
+ })
+ }
+}
+
+func ArtifactsV4Routes(prefix string) *web.Route {
+ m := web.NewRoute()
+
+ r := artifactV4Routes{
+ prefix: prefix,
+ fs: storage.ActionsArtifacts,
+ }
+
+ m.Group("", func() {
+ m.Post("CreateArtifact", r.createArtifact)
+ m.Post("FinalizeArtifact", r.finalizeArtifact)
+ m.Post("ListArtifacts", r.listArtifacts)
+ m.Post("GetSignedArtifactURL", r.getSignedArtifactURL)
+ m.Post("DeleteArtifact", r.deleteArtifact)
+ }, ArtifactContexter())
+ m.Group("", func() {
+ m.Put("UploadArtifact", r.uploadArtifact)
+ m.Get("DownloadArtifact", r.downloadArtifact)
+ }, ArtifactV4Contexter())
+
+ return m
+}
+
+func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte {
+ mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
+ mac.Write([]byte(endp))
+ mac.Write([]byte(expires))
+ mac.Write([]byte(artifactName))
+ mac.Write([]byte(fmt.Sprint(taskID)))
+ return mac.Sum(nil)
+}
+
+func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string {
+ expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
+ uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") +
+ "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
+ return uploadURL
+}
+
+func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) {
+ rawTaskID := ctx.Req.URL.Query().Get("taskID")
+ sig := ctx.Req.URL.Query().Get("sig")
+ expires := ctx.Req.URL.Query().Get("expires")
+ artifactName := ctx.Req.URL.Query().Get("artifactName")
+ dsig, _ := base64.URLEncoding.DecodeString(sig)
+ taskID, _ := strconv.ParseInt(rawTaskID, 10, 64)
+
+ expecedsig := r.buildSignature(endp, expires, artifactName, taskID)
+ if !hmac.Equal(dsig, expecedsig) {
+ log.Error("Error unauthorized")
+ ctx.Error(http.StatusUnauthorized, "Error unauthorized")
+ return nil, "", false
+ }
+ t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires)
+ if err != nil || t.Before(time.Now()) {
+ log.Error("Error link expired")
+ ctx.Error(http.StatusUnauthorized, "Error link expired")
+ return nil, "", false
+ }
+ task, err := actions.GetTaskByID(ctx, taskID)
+ if err != nil {
+ log.Error("Error runner api getting task by ID: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
+ return nil, "", false
+ }
+ if task.Status != actions.StatusRunning {
+ log.Error("Error runner api getting task: task is not running")
+ ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
+ return nil, "", false
+ }
+ if err := task.LoadJob(ctx); err != nil {
+ log.Error("Error runner api getting job: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error runner api getting job")
+ return nil, "", false
+ }
+ return task, artifactName, true
+}
+
+func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) {
+ var art actions.ActionArtifact
+ has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, util.ErrNotExist
+ }
+ return &art, nil
+}
+
+func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
+ body, err := io.ReadAll(ctx.Req.Body)
+ if err != nil {
+ log.Error("Error decode request body: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error decode request body")
+ return false
+ }
+ err = protojson.Unmarshal(body, req)
+ if err != nil {
+ log.Error("Error decode request body: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error decode request body")
+ return false
+ }
+ return true
+}
+
+func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
+ resp, err := protojson.Marshal(req)
+ if err != nil {
+ log.Error("Error encode response body: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error encode response body")
+ return
+ }
+ ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
+ ctx.Resp.WriteHeader(http.StatusOK)
+ _, _ = ctx.Resp.Write(resp)
+}
+
+func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
+ var req CreateArtifactRequest
+
+ if ok := r.parseProtbufBody(ctx, &req); !ok {
+ return
+ }
+ _, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+ if !ok {
+ return
+ }
+
+ artifactName := req.Name
+
+ rententionDays := setting.Actions.ArtifactRetentionDays
+ if req.ExpiresAt != nil {
+ rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24)
+ }
+ // create or get artifact with name and path
+ artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays)
+ if err != nil {
+ log.Error("Error create or get artifact: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error create or get artifact")
+ return
+ }
+ artifact.ContentEncoding = ArtifactV4ContentEncoding
+ if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+ log.Error("Error UpdateArtifactByID: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
+ return
+ }
+
+ respData := CreateArtifactResponse{
+ Ok: true,
+ SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID),
+ }
+ r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
+ task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact")
+ if !ok {
+ return
+ }
+
+ comp := ctx.Req.URL.Query().Get("comp")
+ switch comp {
+ case "block", "appendBlock":
+ // get artifact by name
+ artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
+ if err != nil {
+ log.Error("Error artifact not found: %v", err)
+ ctx.Error(http.StatusNotFound, "Error artifact not found")
+ return
+ }
+
+ if comp == "block" {
+ artifact.FileSize = 0
+ artifact.FileCompressedSize = 0
+ }
+
+ _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID)
+ if err != nil {
+ log.Error("Error runner api getting task: task is not running")
+ ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
+ return
+ }
+ artifact.FileCompressedSize += ctx.Req.ContentLength
+ artifact.FileSize += ctx.Req.ContentLength
+ if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
+ log.Error("Error UpdateArtifactByID: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
+ return
+ }
+ ctx.JSON(http.StatusCreated, "appended")
+ case "blocklist":
+ ctx.JSON(http.StatusCreated, "created")
+ }
+}
+
+func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
+ var req FinalizeArtifactRequest
+
+ if ok := r.parseProtbufBody(ctx, &req); !ok {
+ return
+ }
+ _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+ if !ok {
+ return
+ }
+
+ // get artifact by name
+ artifact, err := r.getArtifactByName(ctx, runID, req.Name)
+ if err != nil {
+ log.Error("Error artifact not found: %v", err)
+ ctx.Error(http.StatusNotFound, "Error artifact not found")
+ return
+ }
+ chunkMap, err := listChunksByRunID(r.fs, runID)
+ if err != nil {
+ log.Error("Error merge chunks: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+ return
+ }
+ chunks, ok := chunkMap[artifact.ID]
+ if !ok {
+ log.Error("Error merge chunks")
+ ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+ return
+ }
+ checksum := ""
+ if req.Hash != nil {
+ checksum = req.Hash.Value
+ }
+ if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil {
+ log.Error("Error merge chunks: %v", err)
+ ctx.Error(http.StatusInternalServerError, "Error merge chunks")
+ return
+ }
+
+ respData := FinalizeArtifactResponse{
+ Ok: true,
+ ArtifactId: artifact.ID,
+ }
+ r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
+ var req ListArtifactsRequest
+
+ if ok := r.parseProtbufBody(ctx, &req); !ok {
+ return
+ }
+ _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+ if !ok {
+ return
+ }
+
+ artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID})
+ if err != nil {
+ log.Error("Error getting artifacts: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ if len(artifacts) == 0 {
+ log.Debug("[artifact] handleListArtifacts, no artifacts")
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
+ list := []*ListArtifactsResponse_MonolithArtifact{}
+
+ table := map[string]*ListArtifactsResponse_MonolithArtifact{}
+ for _, artifact := range artifacts {
+ if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding {
+ table[artifact.ArtifactName] = nil
+ continue
+ }
+
+ table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{
+ Name: artifact.ArtifactName,
+ CreatedAt: timestamppb.New(artifact.CreatedUnix.AsTime()),
+ DatabaseId: artifact.ID,
+ WorkflowRunBackendId: req.WorkflowRunBackendId,
+ WorkflowJobRunBackendId: req.WorkflowJobRunBackendId,
+ Size: artifact.FileSize,
+ }
+ }
+ for _, artifact := range table {
+ if artifact != nil {
+ list = append(list, artifact)
+ }
+ }
+
+ respData := ListArtifactsResponse{
+ Artifacts: list,
+ }
+ r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
+ var req GetSignedArtifactURLRequest
+
+ if ok := r.parseProtbufBody(ctx, &req); !ok {
+ return
+ }
+ _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+ if !ok {
+ return
+ }
+
+ artifactName := req.Name
+
+ // get artifact by name
+ artifact, err := r.getArtifactByName(ctx, runID, artifactName)
+ if err != nil {
+ log.Error("Error artifact not found: %v", err)
+ ctx.Error(http.StatusNotFound, "Error artifact not found")
+ return
+ }
+
+ respData := GetSignedArtifactURLResponse{}
+
+ if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+ u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath)
+ if u != nil && err == nil {
+ respData.SignedUrl = u.String()
+ }
+ }
+ if respData.SignedUrl == "" {
+ respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID)
+ }
+ r.sendProtbufBody(ctx, &respData)
+}
+
+func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
+ task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact")
+ if !ok {
+ return
+ }
+
+ // get artifact by name
+ artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
+ if err != nil {
+ log.Error("Error artifact not found: %v", err)
+ ctx.Error(http.StatusNotFound, "Error artifact not found")
+ return
+ }
+
+ file, _ := r.fs.Open(artifact.StoragePath)
+
+ _, _ = io.Copy(ctx.Resp, file)
+}
+
+func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
+ var req DeleteArtifactRequest
+
+ if ok := r.parseProtbufBody(ctx, &req); !ok {
+ return
+ }
+ _, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
+ if !ok {
+ return
+ }
+
+ // get artifact by name
+ artifact, err := r.getArtifactByName(ctx, runID, req.Name)
+ if err != nil {
+ log.Error("Error artifact not found: %v", err)
+ ctx.Error(http.StatusNotFound, "Error artifact not found")
+ return
+ }
+
+ err = actions.SetArtifactNeedDelete(ctx, runID, req.Name)
+ if err != nil {
+ log.Error("Error deleting artifacts: %v", err)
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ respData := DeleteArtifactResponse{
+ Ok: true,
+ ArtifactId: artifact.ID,
+ }
+ r.sendProtbufBody(ctx, &respData)
+}
diff --git a/routers/api/actions/ping/ping.go b/routers/api/actions/ping/ping.go
index 55219fe12b..828350407a 100644
--- a/routers/api/actions/ping/ping.go
+++ b/routers/api/actions/ping/ping.go
@@ -12,7 +12,7 @@ import (
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
- "github.com/bufbuild/connect-go"
+ "connectrpc.com/connect"
)
func NewPingServiceHandler() (string, http.Handler) {
@@ -21,9 +21,7 @@ func NewPingServiceHandler() (string, http.Handler) {
var _ pingv1connect.PingServiceHandler = (*Service)(nil)
-type Service struct {
- pingv1connect.UnimplementedPingServiceHandler
-}
+type Service struct{}
func (s *Service) Ping(
ctx context.Context,
diff --git a/routers/api/actions/ping/ping_test.go b/routers/api/actions/ping/ping_test.go
index f39e94a1f3..098b003ea2 100644
--- a/routers/api/actions/ping/ping_test.go
+++ b/routers/api/actions/ping/ping_test.go
@@ -11,7 +11,7 @@ import (
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
- "github.com/bufbuild/connect-go"
+ "connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
diff --git a/routers/api/actions/runner/interceptor.go b/routers/api/actions/runner/interceptor.go
index ddc754dbc7..c2f4ade174 100644
--- a/routers/api/actions/runner/interceptor.go
+++ b/routers/api/actions/runner/interceptor.go
@@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
- "github.com/bufbuild/connect-go"
+ "connectrpc.com/connect"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index 8df6f297ce..b2f3e7af78 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -9,6 +9,8 @@ import (
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
@@ -16,7 +18,7 @@ import (
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
- "github.com/bufbuild/connect-go"
+ "connectrpc.com/connect"
gouuid "github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -32,9 +34,7 @@ func NewRunnerServiceHandler() (string, http.Handler) {
var _ runnerv1connect.RunnerServiceClient = (*Service)(nil)
-type Service struct {
- runnerv1connect.UnimplementedRunnerServiceHandler
-}
+type Service struct{}
// Register for new runner.
func (s *Service) Register(
@@ -54,6 +54,18 @@ func (s *Service) Register(
return nil, errors.New("runner registration token has been invalidated, please use the latest one")
}
+ if runnerToken.OwnerID > 0 {
+ if _, err := user_model.GetUserByID(ctx, runnerToken.OwnerID); err != nil {
+ return nil, errors.New("owner of the token not found")
+ }
+ }
+
+ if runnerToken.RepoID > 0 {
+ if _, err := repo_model.GetRepositoryByID(ctx, runnerToken.RepoID); err != nil {
+ return nil, errors.New("repository of the token not found")
+ }
+ }
+
labels := req.Msg.Labels
// TODO: agent_labels should be removed from pb after Gitea 1.20 released.
// Old version runner's agent_labels slice is not empty and labels slice is empty.
diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go
index a7cb31288c..ff6ec5bd54 100644
--- a/routers/api/actions/runner/utils.go
+++ b/routers/api/actions/runner/utils.go
@@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
- secret_module "code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/actions"
@@ -32,14 +31,24 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
return nil, false, nil
}
+ secrets, err := secret_model.GetSecretsOfTask(ctx, t)
+ if err != nil {
+ return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
+ }
+
+ vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
+ if err != nil {
+ return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
+ }
+
actions.CreateCommitStatus(ctx, t.Job)
task := &runnerv1.Task{
Id: t.ID,
WorkflowPayload: t.Job.WorkflowPayload,
Context: generateTaskContext(t),
- Secrets: getSecretsOfTask(ctx, t),
- Vars: getVariablesOfTask(ctx, t),
+ Secrets: secrets,
+ Vars: vars,
}
if needs, err := findTaskNeeds(ctx, t); err != nil {
@@ -55,71 +64,6 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
return task, true, nil
}
-func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
- secrets := map[string]string{}
-
- secrets["GITHUB_TOKEN"] = task.Token
- secrets["GITEA_TOKEN"] = task.Token
-
- if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
- // ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
- // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
- // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
- return secrets
- }
-
- ownerSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
- if err != nil {
- log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
- // go on
- }
- repoSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID})
- if err != nil {
- log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
- // go on
- }
-
- for _, secret := range append(ownerSecrets, repoSecrets...) {
- if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil {
- log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
- // go on
- } else {
- secrets[secret.Name] = v
- }
- }
-
- return secrets
-}
-
-func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
- variables := map[string]string{}
-
- // Global
- globalVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{})
- if err != nil {
- log.Error("find global variables: %v", err)
- }
-
- // Org / User level
- ownerVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
- if err != nil {
- log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err)
- }
-
- // Repo level
- repoVariables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID})
- if err != nil {
- log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err)
- }
-
- // Level precedence: Repo > Org / User > Global
- for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) {
- variables[v.Name] = v.Data
- }
-
- return variables
-}
-
func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
event := map[string]any{}
_ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event)
diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go
index bb14c5163a..dae9c3dfcb 100644
--- a/routers/api/packages/alpine/alpine.go
+++ b/routers/api/packages/alpine/alpine.go
@@ -14,12 +14,12 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
alpine_service "code.gitea.io/gitea/services/packages/alpine"
)
@@ -72,7 +72,7 @@ func GetRepositoryFile(ctx *context.Context) {
ctx,
pv,
&packages_service.PackageFileInfo{
- Filename: alpine_service.IndexFilename,
+ Filename: alpine_service.IndexArchiveFilename,
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
},
)
@@ -182,19 +182,38 @@ func UploadPackageFile(ctx *context.Context) {
}
func DownloadPackageFile(ctx *context.Context) {
- pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
+ branch := ctx.Params("branch")
+ repository := ctx.Params("repository")
+ architecture := ctx.Params("architecture")
+
+ opts := &packages_model.PackageFileSearchOptions{
OwnerID: ctx.Package.Owner.ID,
PackageType: packages_model.TypeAlpine,
Query: ctx.Params("filename"),
- CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
- })
+ CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
+ }
+ pfs, _, err := packages_model.SearchFiles(ctx, opts)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
- if len(pfs) != 1 {
- apiError(ctx, http.StatusNotFound, nil)
- return
+ if len(pfs) == 0 {
+ // Try again with architecture 'noarch'
+ if architecture == alpine_module.NoArch {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ opts.CompositeKey = fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch)
+ if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pfs) == 0 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
}
s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index d990ebb56a..5e3cbac8f9 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -10,7 +10,6 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
@@ -36,7 +35,7 @@ import (
"code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
- context_service "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context"
)
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
@@ -642,7 +641,7 @@ func CommonRoutes() *web.Route {
})
})
}, reqPackageAccess(perm.AccessModeRead))
- }, context_service.UserAssignmentWeb(), context.PackageAssignment())
+ }, context.UserAssignmentWeb(), context.PackageAssignment())
return r
}
@@ -812,7 +811,7 @@ func ContainerRoutes() *web.Route {
ctx.Status(http.StatusNotFound)
})
- }, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
+ }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
return r
}
diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go
index 8f1e965c9a..140e532efd 100644
--- a/routers/api/packages/cargo/cargo.go
+++ b/routers/api/packages/cargo/cargo.go
@@ -12,14 +12,15 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
cargo_service "code.gitea.io/gitea/services/packages/cargo"
@@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeCargo,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: &paginator,
},
)
diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go
index 3aef8281a4..a790e9a363 100644
--- a/routers/api/packages/chef/auth.go
+++ b/routers/api/packages/chef/auth.go
@@ -8,6 +8,7 @@ import (
"crypto"
"crypto/rsa"
"crypto/sha1"
+ "crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
@@ -26,8 +27,6 @@ import (
chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth"
-
- "github.com/minio/sha256-simd"
)
const (
diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go
index f1e9ae12d8..b49f4e9d0a 100644
--- a/routers/api/packages/chef/chef.go
+++ b/routers/api/packages/chef/chef.go
@@ -15,12 +15,13 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeChef,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeChef,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("start"),
ctx.FormInt("items"),
diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go
index 0551093cd1..a045da40de 100644
--- a/routers/api/packages/composer/composer.go
+++ b/routers/api/packages/composer/composer.go
@@ -14,12 +14,13 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
@@ -66,7 +67,7 @@ func SearchPackages(ctx *context.Context) {
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeComposer,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: &paginator,
}
if ctx.FormTrim("type") != "" {
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
index 4bf13222dc..c45e085a4d 100644
--- a/routers/api/packages/conan/conan.go
+++ b/routers/api/packages/conan/conan.go
@@ -15,13 +15,13 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
conan_model "code.gitea.io/gitea/models/packages/conan"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
)
diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go
index 2bcf9df162..7370c702cd 100644
--- a/routers/api/packages/conan/search.go
+++ b/routers/api/packages/conan/search.go
@@ -9,9 +9,9 @@ import (
conan_model "code.gitea.io/gitea/models/packages/conan"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
conan_module "code.gitea.io/gitea/modules/packages/conan"
+ "code.gitea.io/gitea/services/context"
)
// SearchResult contains the found recipe names
diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go
index 0bee7baa96..30c80fc15e 100644
--- a/routers/api/packages/conda/conda.go
+++ b/routers/api/packages/conda/conda.go
@@ -12,13 +12,13 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
conda_model "code.gitea.io/gitea/models/packages/conda"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
conda_module "code.gitea.io/gitea/modules/packages/conda"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/dsnet/compress/bzip2"
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 8621242da4..e519766142 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -17,7 +17,6 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
@@ -25,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
diff --git a/routers/api/packages/cran/cran.go b/routers/api/packages/cran/cran.go
index ae43df7c9a..2cec75294f 100644
--- a/routers/api/packages/cran/cran.go
+++ b/routers/api/packages/cran/cran.go
@@ -13,11 +13,11 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
cran_model "code.gitea.io/gitea/models/packages/cran"
- "code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
cran_module "code.gitea.io/gitea/modules/packages/cran"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go
index 379137e87e..241de3ac5d 100644
--- a/routers/api/packages/debian/debian.go
+++ b/routers/api/packages/debian/debian.go
@@ -13,11 +13,11 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
debian_module "code.gitea.io/gitea/modules/packages/debian"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
debian_service "code.gitea.io/gitea/services/packages/debian"
diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go
index 30854335c0..8232931134 100644
--- a/routers/api/packages/generic/generic.go
+++ b/routers/api/packages/generic/generic.go
@@ -8,18 +8,19 @@ import (
"net/http"
"regexp"
"strings"
+ "unicode"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
var (
- packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`)
- filenameRegex = packageNameRegex
+ packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
+ filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -54,20 +55,38 @@ func DownloadPackageFile(ctx *context.Context) {
helper.ServePackageFile(ctx, s, u, pf)
}
+func isValidPackageName(packageName string) bool {
+ if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
+ return false
+ }
+ return packageNameRegex.MatchString(packageName) && packageName != ".."
+}
+
+func isValidFileName(filename string) bool {
+ return filenameRegex.MatchString(filename) &&
+ strings.TrimSpace(filename) == filename &&
+ filename != "." && filename != ".."
+}
+
// UploadPackage uploads the specific generic package.
// Duplicated packages get rejected.
func UploadPackage(ctx *context.Context) {
packageName := ctx.Params("packagename")
filename := ctx.Params("filename")
- if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) {
- apiError(ctx, http.StatusBadRequest, errors.New("Invalid package name or filename"))
+ if !isValidPackageName(packageName) {
+ apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
+ return
+ }
+
+ if !isValidFileName(filename) {
+ apiError(ctx, http.StatusBadRequest, errors.New("invalid filename"))
return
}
packageVersion := ctx.Params("packageversion")
if packageVersion != strings.TrimSpace(packageVersion) {
- apiError(ctx, http.StatusBadRequest, errors.New("Invalid package version"))
+ apiError(ctx, http.StatusBadRequest, errors.New("invalid package version"))
return
}
diff --git a/routers/api/packages/generic/generic_test.go b/routers/api/packages/generic/generic_test.go
new file mode 100644
index 0000000000..1acaafe576
--- /dev/null
+++ b/routers/api/packages/generic/generic_test.go
@@ -0,0 +1,65 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package generic
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestValidatePackageName(t *testing.T) {
+ bad := []string{
+ "",
+ ".",
+ "..",
+ "-",
+ "a?b",
+ "a b",
+ "a/b",
+ }
+ for _, name := range bad {
+ assert.False(t, isValidPackageName(name), "bad=%q", name)
+ }
+
+ good := []string{
+ "a",
+ "1",
+ "a-",
+ "a_b",
+ "c.d+",
+ }
+ for _, name := range good {
+ assert.True(t, isValidPackageName(name), "good=%q", name)
+ }
+}
+
+func TestValidateFileName(t *testing.T) {
+ bad := []string{
+ "",
+ ".",
+ "..",
+ "a?b",
+ "a/b",
+ " a",
+ "a ",
+ }
+ for _, name := range bad {
+ assert.False(t, isValidFileName(name), "bad=%q", name)
+ }
+
+ good := []string{
+ "-",
+ "a",
+ "1",
+ "a-",
+ "a_b",
+ "a b",
+ "c.d+",
+ `-_+=:;.()[]{}~!@#$%^& aA1`,
+ }
+ for _, name := range good {
+ assert.True(t, isValidFileName(name), "good=%q", name)
+ }
+}
diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go
index 18e0074ab4..d658066bb4 100644
--- a/routers/api/packages/goproxy/goproxy.go
+++ b/routers/api/packages/goproxy/goproxy.go
@@ -12,11 +12,12 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -129,7 +130,7 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (
Value: name,
ExactMatch: true,
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
})
if err != nil {
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
index a8daa69dc3..efdb83ec0e 100644
--- a/routers/api/packages/helm/helm.go
+++ b/routers/api/packages/helm/helm.go
@@ -13,14 +13,15 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
helm_module "code.gitea.io/gitea/modules/packages/helm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"gopkg.in/yaml.v3"
@@ -42,7 +43,7 @@ func Index(ctx *context.Context) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeHelm,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -110,7 +111,7 @@ func DownloadPackageFile(ctx *context.Context) {
Value: ctx.Params("package"),
},
HasFileWithName: filename,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go
index aadb10376c..cdb64109ad 100644
--- a/routers/api/packages/helper/helper.go
+++ b/routers/api/packages/helper/helper.go
@@ -10,9 +10,9 @@ import (
"net/url"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
// LogAndProcessError logs an error and calls a custom callback with the processed error message.
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 0b93382b01..27f0578db7 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -6,6 +6,7 @@ package maven
import (
"crypto/md5"
"crypto/sha1"
+ "crypto/sha256"
"crypto/sha512"
"encoding/hex"
"encoding/xml"
@@ -19,15 +20,13 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
maven_module "code.gitea.io/gitea/modules/packages/maven"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
-
- "github.com/minio/sha256-simd"
)
const (
diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go
index 8470874884..f8e839c424 100644
--- a/routers/api/packages/npm/api.go
+++ b/routers/api/packages/npm/api.go
@@ -12,6 +12,7 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
+ "code.gitea.io/gitea/modules/setting"
)
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
@@ -98,7 +99,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
Maintainers: []npm_module.User{}, // npm cli needs this field
Keywords: metadata.Keywords,
Links: &npm_module.PackageSearchPackageLinks{
- Registry: pd.FullWebLink(),
+ Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
Homepage: metadata.ProjectURL,
},
},
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index 170edfbe11..84acfffae2 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -17,12 +17,13 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
@@ -120,7 +121,7 @@ func DownloadPackageFileByName(ctx *context.Context) {
Value: packageNameFromParams(ctx),
},
HasFileWithName: filename,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -395,7 +396,7 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
Properties: map[string]string{
npm_module.TagProperty: tag,
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
return err
@@ -431,7 +432,7 @@ func PackageSearch(ctx *context.Context) {
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNpm,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Name: packages_model.SearchValue{
ExactMatch: false,
Value: ctx.FormTrim("text"),
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index 769c4c1824..c28bc6c9d9 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -17,13 +17,14 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
nuget_model "code.gitea.io/gitea/models/packages/nuget"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -122,7 +123,7 @@ func SearchServiceV2(ctx *context.Context) {
Name: packages_model.SearchValue{
Value: getSearchTerm(ctx),
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: paginator,
})
if err != nil {
@@ -172,7 +173,7 @@ func SearchServiceV2Count(ctx *context.Context) {
Name: packages_model.SearchValue{
Value: getSearchTerm(ctx),
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -187,7 +188,7 @@ func SearchServiceV3(ctx *context.Context) {
pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("skip"),
ctx.FormInt("take"),
@@ -313,7 +314,7 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
ExactMatch: true,
Value: packageName,
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: paginator,
})
if err != nil {
@@ -358,7 +359,7 @@ func EnumeratePackageVersionsV2Count(ctx *context.Context) {
ExactMatch: true,
Value: strings.Trim(ctx.FormTrim("id"), "'"),
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go
index 1f605c6c9f..f87df52a29 100644
--- a/routers/api/packages/pub/pub.go
+++ b/routers/api/packages/pub/pub.go
@@ -14,7 +14,6 @@ import (
"time"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
@@ -22,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 5718b1203b..7824db1823 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -12,12 +12,12 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
pypi_module "code.gitea.io/gitea/modules/packages/pypi"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
index 5d06680552..4de361c214 100644
--- a/routers/api/packages/rpm/rpm.go
+++ b/routers/api/packages/rpm/rpm.go
@@ -13,13 +13,13 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
rpm_service "code.gitea.io/gitea/services/packages/rpm"
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
index 01fd4dad66..d2fbcd01f0 100644
--- a/routers/api/packages/rubygems/rubygems.go
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -13,11 +13,12 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -43,7 +44,7 @@ func EnumeratePackagesLatest(ctx *context.Context) {
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -304,7 +305,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
HasFileWithName: filename,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
return pvs, err
}
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 427e262d06..a9da3ea9c2 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -13,14 +13,15 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
swift_module "code.gitea.io/gitea/modules/packages/swift"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
@@ -157,7 +158,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
}
type Resource struct {
- Name string `json:"id"`
+ Name string `json:"name"`
Type string `json:"type"`
Checksum string `json:"checksum"`
}
@@ -433,7 +434,7 @@ func LookupPackageIdentifiers(ctx *context.Context) {
Properties: map[string]string{
swift_module.PropertyRepositoryURL: url,
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go
index af9cd08a62..98a81da368 100644
--- a/routers/api/packages/vagrant/vagrant.go
+++ b/routers/api/packages/vagrant/vagrant.go
@@ -12,11 +12,11 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go
index cad5032d10..995a148f0b 100644
--- a/routers/api/v1/activitypub/person.go
+++ b/routers/api/v1/activitypub/person.go
@@ -9,9 +9,9 @@ import (
"strings"
"code.gitea.io/gitea/modules/activitypub"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
ap "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go
index 3f60ed7776..59ebc74b89 100644
--- a/routers/api/v1/activitypub/reqsignature.go
+++ b/routers/api/v1/activitypub/reqsignature.go
@@ -13,9 +13,9 @@ import (
"net/url"
"code.gitea.io/gitea/modules/activitypub"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
+ gitea_context "code.gitea.io/gitea/services/context"
ap "github.com/go-ap/activitypub"
"github.com/go-fed/httpsig"
diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go
index bf030eb222..a4708fe032 100644
--- a/routers/api/v1/admin/adopt.go
+++ b/routers/api/v1/admin/adopt.go
@@ -8,9 +8,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go
index cc8c6c9e23..e1ca6048c9 100644
--- a/routers/api/v1/admin/cron.go
+++ b/routers/api/v1/admin/cron.go
@@ -6,11 +6,11 @@ package admin
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/cron"
)
diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go
index 5914215bc2..ba963e9f69 100644
--- a/routers/api/v1/admin/email.go
+++ b/routers/api/v1/admin/email.go
@@ -7,9 +7,9 @@ import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go
index 8a095a7def..4c168b55bf 100644
--- a/routers/api/v1/admin/hooks.go
+++ b/routers/api/v1/admin/hooks.go
@@ -8,12 +8,13 @@ import (
"net/http"
"code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
)
@@ -37,7 +38,7 @@ func ListHooks(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/HookList"
- sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+ sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]())
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
return
diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go
index bf68942a9c..a5c299bbf0 100644
--- a/routers/api/v1/admin/org.go
+++ b/routers/api/v1/admin/org.go
@@ -10,10 +10,10 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go
index a4895f260b..c119d5390a 100644
--- a/routers/api/v1/admin/repo.go
+++ b/routers/api/v1/admin/repo.go
@@ -4,10 +4,10 @@
package admin
import (
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/repo"
+ "code.gitea.io/gitea/services/context"
)
// CreateRepo api for creating a repository
diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go
index c0d9364435..329242d9f6 100644
--- a/routers/api/v1/admin/runners.go
+++ b/routers/api/v1/admin/runners.go
@@ -4,8 +4,8 @@
package admin
import (
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 272996f43d..87a5b28fad 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -15,17 +15,16 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
@@ -117,11 +116,8 @@ func CreateUser(ctx *context.APIContext) {
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolTrue,
- }
-
- if form.Restricted != nil {
- overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
+ IsActive: optional.Some(true),
+ IsRestricted: optional.FromPtr(form.Restricted),
}
if form.Visibility != "" {
@@ -137,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
u.UpdatedUnix = u.CreatedUnix
}
- if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
+ if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
user_model.IsErrEmailAlreadyUsed(err) ||
db.IsErrNameReserved(err) ||
@@ -151,6 +147,11 @@ func CreateUser(ctx *context.APIContext) {
}
return
}
+
+ if !user_model.IsEmailDomainAllowed(u.Email) {
+ ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
+ }
+
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
@@ -213,7 +214,7 @@ func EditUser(ctx *context.APIContext) {
}
if form.Email != nil {
- if err := user_service.AddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
+ if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
@@ -224,6 +225,10 @@ func EditUser(ctx *context.APIContext) {
}
return
}
+
+ if !user_model.IsEmailDomainAllowed(*form.Email) {
+ ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
+ }
}
opts := &user_service.UpdateOptions{
diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go
new file mode 100644
index 0000000000..bacd1f809b
--- /dev/null
+++ b/routers/api/v1/admin/user_badge.go
@@ -0,0 +1,124 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+)
+
+// ListUserBadges lists all badges belonging to a user
+func ListUserBadges(ctx *context.APIContext) {
+ // swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
+ // ---
+ // summary: List a user's badges
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of user
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/BadgeList"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetUserBadges", err)
+ return
+ }
+
+ ctx.SetTotalCountHeader(maxResults)
+ ctx.JSON(http.StatusOK, &badges)
+}
+
+// AddUserBadges add badges to a user
+func AddUserBadges(ctx *context.APIContext) {
+ // swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
+ // ---
+ // summary: Add a badge to a user
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of user
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UserBadgeOption"
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ form := web.GetForm(ctx).(*api.UserBadgeOption)
+ badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+ if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// DeleteUserBadges delete a badge from a user
+func DeleteUserBadges(ctx *context.APIContext) {
+ // swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
+ // ---
+ // summary: Remove a badge from a user
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of user
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UserBadgeOption"
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form := web.GetForm(ctx).(*api.UserBadgeOption)
+ badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+ if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge {
+ badges := make([]*user_model.Badge, len(form.BadgeSlugs))
+ for i, badge := range form.BadgeSlugs {
+ badges[i] = &user_model.Badge{
+ Slug: badge,
+ }
+ }
+ return badges
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index cb1803f7c6..e870378c4b 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -6,9 +6,9 @@
//
// This documentation describes the Gitea API.
//
-// Schemes: http, https
+// Schemes: https, http
// BasePath: /api/v1
-// Version: {{AppVer | JSEscape | Safe}}
+// Version: {{AppVer | JSEscape}}
// License: MIT http://opensource.org/licenses/MIT
//
// Consumes:
@@ -79,7 +79,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -95,7 +94,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/auth"
- context_service "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
@@ -811,7 +810,7 @@ func individualPermsChecker(ctx *context.APIContext) {
// check for and warn against deprecated authentication options
func checkDeprecatedAuthMethods(ctx *context.APIContext) {
if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" {
- ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
+ ctx.Resp.Header().Set("X-Gitea-Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
}
}
@@ -855,11 +854,11 @@ func Routes() *web.Route {
m.Group("/user/{username}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
- }, context_service.UserAssignmentAPI())
+ }, context.UserAssignmentAPI())
m.Group("/user-id/{user-id}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
- }, context_service.UserIDAssignmentAPI())
+ }, context.UserIDAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
}
@@ -915,7 +914,7 @@ func Routes() *web.Route {
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
m.Get("/activities/feeds", user.ListUserActivityFeeds)
- }, context_service.UserAssignmentAPI(), individualPermsChecker)
+ }, context.UserAssignmentAPI(), individualPermsChecker)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
// Users (requires user scope)
@@ -933,7 +932,7 @@ func Routes() *web.Route {
m.Get("/starred", user.GetStarredRepos)
m.Get("/subscriptions", user.GetWatchedRepos)
- }, context_service.UserAssignmentAPI())
+ }, context.UserAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Users (requires user scope)
@@ -956,6 +955,15 @@ func Routes() *web.Route {
Delete(user.DeleteSecret)
})
+ m.Group("/variables", func() {
+ m.Get("", user.ListVariables)
+ m.Combo("/{variablename}").
+ Get(user.GetVariable).
+ Delete(user.DeleteVariable).
+ Post(bind(api.CreateVariableOption{}), user.CreateVariable).
+ Put(bind(api.UpdateVariableOption{}), user.UpdateVariable)
+ })
+
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
})
@@ -968,7 +976,7 @@ func Routes() *web.Route {
m.Get("", user.CheckMyFollowing)
m.Put("", user.Follow)
m.Delete("", user.Unfollow)
- }, context_service.UserAssignmentAPI())
+ }, context.UserAssignmentAPI())
})
// (admin:public_key scope)
@@ -1028,7 +1036,16 @@ func Routes() *web.Route {
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar)
- }, reqToken())
+ })
+
+ m.Group("/blocks", func() {
+ m.Get("", user.ListBlocks)
+ m.Group("/{username}", func() {
+ m.Get("", user.CheckUserBlock)
+ m.Put("", user.BlockUser)
+ m.Delete("", user.UnblockUser)
+ }, context.UserAssignmentAPI())
+ })
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories (requires repo scope, org scope)
@@ -1065,6 +1082,15 @@ func Routes() *web.Route {
Delete(reqToken(), reqOwner(), repo.DeleteSecret)
})
+ m.Group("/variables", func() {
+ m.Get("", reqToken(), reqOwner(), repo.ListVariables)
+ m.Combo("/{variablename}").
+ Get(reqToken(), reqOwner(), repo.GetVariable).
+ Delete(reqToken(), reqOwner(), repo.DeleteVariable).
+ Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
+ Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
+ })
+
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
})
@@ -1225,6 +1251,7 @@ func Routes() *web.Route {
Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
})
+ m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
m.Group("/statuses", func() {
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
@@ -1235,6 +1262,7 @@ func Routes() *web.Route {
m.Group("/{ref}", func() {
m.Get("/status", repo.GetCombinedCommitStatusByRef)
m.Get("/statuses", repo.GetCommitStatusesByRef)
+ m.Get("/pull", repo.GetCommitPullRequest)
}, context.ReferencesGitRepo())
}, reqRepoReader(unit.TypeCode))
m.Group("/git", func() {
@@ -1413,14 +1441,14 @@ func Routes() *web.Route {
m.Get("/files", reqToken(), packages.ListPackageFiles)
})
m.Get("/", reqToken(), packages.ListPackages)
- }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
+ }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
- }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
+ }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Group("/orgs/{org}", func() {
@@ -1442,6 +1470,15 @@ func Routes() *web.Route {
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
})
+ m.Group("/variables", func() {
+ m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
+ m.Combo("/{variablename}").
+ Get(reqToken(), reqOrgOwnership(), org.GetVariable).
+ Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
+ Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
+ Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
+ })
+
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
})
@@ -1476,6 +1513,15 @@ func Routes() *web.Route {
m.Delete("", org.DeleteAvatar)
}, reqToken(), reqOrgOwnership())
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
+
+ m.Group("/blocks", func() {
+ m.Get("", org.ListBlocks)
+ m.Group("/{username}", func() {
+ m.Get("", org.CheckUserBlock)
+ m.Put("", org.BlockUser)
+ m.Delete("", org.UnblockUser)
+ })
+ }, reqToken(), reqOrgOwnership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
@@ -1518,7 +1564,10 @@ func Routes() *web.Route {
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
- }, context_service.UserAssignmentAPI())
+ m.Get("/badges", admin.ListUserBadges)
+ m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
+ m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
+ }, context.UserAssignmentAPI())
})
m.Group("/emails", func() {
m.Get("", admin.GetAllEmails)
diff --git a/routers/api/v1/misc/gitignore.go b/routers/api/v1/misc/gitignore.go
index 7c7fe4b125..dffd771752 100644
--- a/routers/api/v1/misc/gitignore.go
+++ b/routers/api/v1/misc/gitignore.go
@@ -6,11 +6,11 @@ package misc
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/options"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
// Shows a list of all Gitignore templates
diff --git a/routers/api/v1/misc/label_templates.go b/routers/api/v1/misc/label_templates.go
index 0e0ca39fc5..cc11f37626 100644
--- a/routers/api/v1/misc/label_templates.go
+++ b/routers/api/v1/misc/label_templates.go
@@ -6,9 +6,9 @@ package misc
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/misc/licenses.go b/routers/api/v1/misc/licenses.go
index 65f63468cf..2a980f5084 100644
--- a/routers/api/v1/misc/licenses.go
+++ b/routers/api/v1/misc/licenses.go
@@ -8,12 +8,12 @@ import (
"net/http"
"net/url"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/options"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
// Returns a list of all License templates
diff --git a/routers/api/v1/misc/markup.go b/routers/api/v1/misc/markup.go
index 7b24b353b6..9699c79368 100644
--- a/routers/api/v1/misc/markup.go
+++ b/routers/api/v1/misc/markup.go
@@ -6,12 +6,12 @@ package misc
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/context"
)
// Markup render markup document to HTML
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index ec8f8f47b7..5236fd06ae 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -10,19 +10,19 @@ import (
"strings"
"testing"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
const (
- AppURL = "http://localhost:3000/"
- Repo = "gogits/gogs"
- AppSubURL = AppURL + Repo + "/"
+ AppURL = "http://localhost:3000/"
+ Repo = "gogits/gogs"
+ FullURL = AppURL + Repo + "/"
)
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
@@ -74,20 +74,20 @@ func TestAPI_RenderGFM(t *testing.T) {
// rendered
`Wiki! Enjoy :)
-- Links, Language bindings, Engine bindings
-- Tips
+- Links, Language bindings, Engine bindings
+- Tips
- Bezier widget (by @r-lyeh) https://github.com/ocornut/imgui/issues/786
`,
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
- `Guardfile-DSL / Configuring-Guard
+ `Guardfile-DSL / Configuring-Guard
`,
// special syntax
`[[Name|Link]]`,
// rendered
- `
+ `
`,
// empty
``,
@@ -111,8 +111,8 @@ Here are some links to the most important topics. You can find the full list of
Wine Staging on website wine-staging.com.
Quick Links
Here are some links to the most important topics. You can find the full list of pages at the sidebar.
-
+
`,
}
diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go
index cc754f64a2..3bd80de5c1 100644
--- a/routers/api/v1/misc/nodeinfo.go
+++ b/routers/api/v1/misc/nodeinfo.go
@@ -9,9 +9,9 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
)
const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go
index 2ca9813e15..24a46c1e70 100644
--- a/routers/api/v1/misc/signing.go
+++ b/routers/api/v1/misc/signing.go
@@ -7,8 +7,8 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/modules/context"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
)
// SigningKey returns the public key of the default signing key if it exists
diff --git a/routers/api/v1/misc/version.go b/routers/api/v1/misc/version.go
index 83fa35219a..e3b43a0e6b 100644
--- a/routers/api/v1/misc/version.go
+++ b/routers/api/v1/misc/version.go
@@ -6,9 +6,9 @@ package misc
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
)
// Version shows the version of the Gitea server
diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go
index c87da9399f..46b3c7f5e7 100644
--- a/routers/api/v1/notify/notifications.go
+++ b/routers/api/v1/notify/notifications.go
@@ -9,9 +9,9 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
)
// NewAvailable check if unread notifications exist
diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go
index 55ca6ad1fd..1744426ee8 100644
--- a/routers/api/v1/notify/repo.go
+++ b/routers/api/v1/notify/repo.go
@@ -10,9 +10,8 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -201,7 +200,6 @@ func ReadRepoNotifications(ctx *context.APIContext) {
if !ctx.FormBool("all") {
statuses := ctx.FormStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
- log.Error("%v", opts.Status)
}
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go
index 919e52952d..8e12d359cb 100644
--- a/routers/api/v1/notify/threads.go
+++ b/routers/api/v1/notify/threads.go
@@ -10,7 +10,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go
index 4abdfb2e92..879f484cce 100644
--- a/routers/api/v1/notify/user.go
+++ b/routers/api/v1/notify/user.go
@@ -9,8 +9,8 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go
index 7b621a50c3..e34c68dfc9 100644
--- a/routers/api/v1/org/avatar.go
+++ b/routers/api/v1/org/avatar.go
@@ -7,9 +7,9 @@ import (
"encoding/base64"
"net/http"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go
new file mode 100644
index 0000000000..69a5222a20
--- /dev/null
+++ b/routers/api/v1/org/block.go
@@ -0,0 +1,116 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
+ // ---
+ // summary: List users blocked by the organization
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/responses/UserList"
+
+ shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
+ // ---
+ // summary: Check if a user is blocked by the organization
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: user to check
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
+}
+
+func BlockUser(ctx *context.APIContext) {
+ // swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
+ // ---
+ // summary: Block a user
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: user to block
+ // type: string
+ // required: true
+ // - name: note
+ // in: query
+ // description: optional note for the block
+ // type: string
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
+}
+
+func UnblockUser(ctx *context.APIContext) {
+ // swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
+ // ---
+ // summary: Unblock a user
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: username
+ // in: path
+ // description: user to unblock
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
+}
diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go
index 3c3f058b5d..c1dc0519ea 100644
--- a/routers/api/v1/org/hook.go
+++ b/routers/api/v1/org/hook.go
@@ -6,10 +6,10 @@ package org
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
)
diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go
index 5a03059ded..b5ec54ccf4 100644
--- a/routers/api/v1/org/label.go
+++ b/routers/api/v1/org/label.go
@@ -9,11 +9,11 @@ import (
"strings"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go
index 422b7cecfe..9db9ad964b 100644
--- a/routers/api/v1/org/member.go
+++ b/routers/api/v1/org/member.go
@@ -9,11 +9,11 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
- if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
+ if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index 255e28c706..e848d95181 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -12,12 +12,12 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/org"
user_service "code.gitea.io/gitea/services/user"
diff --git a/routers/api/v1/org/runners.go b/routers/api/v1/org/runners.go
index 05bce8daef..2a52bd8778 100644
--- a/routers/api/v1/org/runners.go
+++ b/routers/api/v1/org/runners.go
@@ -4,8 +4,8 @@
package org
import (
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/org/secrets.go b/routers/api/v1/org/secrets.go
index ddc74d865b..abb6bb26c4 100644
--- a/routers/api/v1/org/secrets.go
+++ b/routers/api/v1/org/secrets.go
@@ -9,11 +9,11 @@ import (
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index f129c66230..015af774e3 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -15,12 +15,13 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
@@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
@@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
if ctx.Written() {
return
}
- if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
- ctx.Error(http.StatusInternalServerError, "AddMember", err)
+ if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "AddTeamMember", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
+ }
return
}
ctx.Status(http.StatusNoContent)
@@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
return
}
- if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
+ if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
return
}
diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go
new file mode 100644
index 0000000000..eaf7bdc45b
--- /dev/null
+++ b/routers/api/v1/org/variables.go
@@ -0,0 +1,291 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "errors"
+ "net/http"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
+ "code.gitea.io/gitea/services/context"
+)
+
+// ListVariables list org-level variables
+func ListVariables(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
+ // ---
+ // summary: Get an org-level variables list
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/VariableList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+ OwnerID: ctx.Org.Organization.ID,
+ ListOptions: utils.GetListOptions(ctx),
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+ return
+ }
+
+ variables := make([]*api.ActionVariable, len(vars))
+ for i, v := range vars {
+ variables[i] = &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+ }
+
+ ctx.SetTotalCountHeader(count)
+ ctx.JSON(http.StatusOK, variables)
+}
+
+// GetVariable get an org-level variable
+func GetVariable(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
+ // ---
+ // summary: Get an org-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Org.Organization.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ variable := &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+
+ ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete an org-level variable
+func DeleteVariable(ctx *context.APIContext) {
+ // swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
+ // ---
+ // summary: Delete an org-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "201":
+ // description: response when deleting a variable
+ // "204":
+ // description: response when deleting a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create an org-level variable
+func CreateVariable(ctx *context.APIContext) {
+ // swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
+ // ---
+ // summary: Create an org-level variable
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateVariableOption"
+ // responses:
+ // "201":
+ // description: response when creating an org-level variable
+ // "204":
+ // description: response when creating an org-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+ ownerID := ctx.Org.Organization.ID
+ variableName := ctx.Params("variablename")
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ownerID,
+ Name: variableName,
+ })
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ return
+ }
+ if v != nil && v.ID > 0 {
+ ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+ return
+ }
+
+ if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update an org-level variable
+func UpdateVariable(ctx *context.APIContext) {
+ // swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
+ // ---
+ // summary: Update an org-level variable
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UpdateVariableOption"
+ // responses:
+ // "201":
+ // description: response when updating an org-level variable
+ // "204":
+ // description: response when updating an org-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Org.Organization.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ if opt.Name == "" {
+ opt.Name = ctx.Params("variablename")
+ }
+ if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index a79ba315be..b38aa13167 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -7,10 +7,10 @@ import (
"net/http"
"code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -60,7 +60,7 @@ func ListPackages(ctx *context.APIContext) {
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: &listOptions,
})
if err != nil {
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index 039cdadac9..03321d956d 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -7,10 +7,14 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/modules/context"
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
+ "code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -127,3 +131,295 @@ func DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+
+// GetVariable get a repo-level variable
+func GetVariable(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
+ // ---
+ // summary: Get a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ RepoID: ctx.Repo.Repository.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ variable := &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+
+ ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete a repo-level variable
+func DeleteVariable(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
+ // ---
+ // summary: Delete a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "201":
+ // description: response when deleting a variable
+ // "204":
+ // description: response when deleting a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create a repo-level variable
+func CreateVariable(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
+ // ---
+ // summary: Create a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateVariableOption"
+ // responses:
+ // "201":
+ // description: response when creating a repo-level variable
+ // "204":
+ // description: response when creating a repo-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+ repoID := ctx.Repo.Repository.ID
+ variableName := ctx.Params("variablename")
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ RepoID: repoID,
+ Name: variableName,
+ })
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ return
+ }
+ if v != nil && v.ID > 0 {
+ ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+ return
+ }
+
+ if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a repo-level variable
+func UpdateVariable(ctx *context.APIContext) {
+ // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
+ // ---
+ // summary: Update a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UpdateVariableOption"
+ // responses:
+ // "201":
+ // description: response when updating a repo-level variable
+ // "204":
+ // description: response when updating a repo-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ RepoID: ctx.Repo.Repository.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ if opt.Name == "" {
+ opt.Name = ctx.Params("variablename")
+ }
+ if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// ListVariables list repo-level variables
+func ListVariables(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
+ // ---
+ // summary: Get repo-level variables list
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/VariableList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+ RepoID: ctx.Repo.Repository.ID,
+ ListOptions: utils.GetListOptions(ctx),
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+ return
+ }
+
+ variables := make([]*api.ActionVariable, len(vars))
+ for i, v := range vars {
+ variables[i] = &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ }
+ }
+
+ ctx.SetTotalCountHeader(count)
+ ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go
index 1b661955f0..698337ffd2 100644
--- a/routers/api/v1/repo/avatar.go
+++ b/routers/api/v1/repo/avatar.go
@@ -7,9 +7,9 @@ import (
"encoding/base64"
"net/http"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go
index 26605bba03..3b116666ea 100644
--- a/routers/api/v1/repo/blob.go
+++ b/routers/api/v1/repo/blob.go
@@ -6,7 +6,7 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
)
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index 21e4b05a8b..1574cf046c 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -14,14 +14,14 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
@@ -141,7 +141,7 @@ func DeleteBranch(ctx *context.APIContext) {
// check whether branches of this repository has been synced
totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
- IsDeletedBranch: util.OptionalBoolFalse,
+ IsDeletedBranch: optional.Some(false),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "CountBranches", err)
@@ -340,7 +340,7 @@ func ListBranches(ctx *context.APIContext) {
branchOpts := git_model.FindBranchOptions{
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
- IsDeletedBranch: util.OptionalBoolFalse,
+ IsDeletedBranch: optional.Some(false),
}
var err error
totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index a222e50a5e..4ce14f7d01 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -8,16 +8,15 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
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"
- "code.gitea.io/gitea/modules/context"
repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -54,15 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- count, err := db.Count[repo_model.Collaboration](ctx, repo_model.FindCollaborationOptions{
- RepoID: ctx.Repo.Repository.ID,
+ collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ RepoID: ctx.Repo.Repository.ID,
})
- if err != nil {
- ctx.InternalServerError(err)
- return
- }
-
- collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return
@@ -73,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
}
- ctx.SetTotalCountHeader(count)
+ ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, users)
}
@@ -159,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
@@ -182,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
}
if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
- ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "AddCollaborator", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
+ }
return
}
@@ -237,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
return
}
- if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
+ if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
return
}
diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go
index 43b6400009..d06a3b4e49 100644
--- a/routers/api/v1/repo/commits.go
+++ b/routers/api/v1/repo/commits.go
@@ -10,12 +10,13 @@ import (
"net/http"
"strconv"
+ issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -323,3 +324,53 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
return
}
}
+
+// GetCommitPullRequest returns the pull request of the commit
+func GetCommitPullRequest(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest
+ // ---
+ // summary: Get the pull request of the commit
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: sha
+ // in: path
+ // description: SHA of the commit to get
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/PullRequest"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.Params(":sha"))
+ if err != nil {
+ if issues_model.IsErrPullRequestNotExist(err) {
+ ctx.Error(http.StatusNotFound, "GetPullRequestByMergedCommit", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
+ }
+ return
+ }
+
+ if err = pr.LoadBaseRepo(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
+ return
+ }
+ if err = pr.LoadHeadRepo(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
+}
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 065d6bf8b2..156033f58a 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -19,7 +19,6 @@ import (
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httpcache"
@@ -30,6 +29,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
files_service "code.gitea.io/gitea/services/repository/files"
)
@@ -145,7 +145,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
return
}
- // OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
+ // OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
dataRc, err := blob.DataAsync()
if err != nil {
ctx.ServerError("DataAsync", err)
@@ -408,7 +408,7 @@ func canReadFiles(r *context.Repository) bool {
return r.Permission.CanRead(unit.TypeCode)
}
-func base64Reader(s string) (io.Reader, error) {
+func base64Reader(s string) (io.ReadSeeker, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, err
@@ -655,6 +655,7 @@ func UpdateFile(ctx *context.APIContext) {
apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
if ctx.Repo.Repository.IsEmpty {
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
+ return
}
if apiOpts.BranchName == "" {
@@ -762,13 +763,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch
}
message := ""
if len(createFiles) != 0 {
- message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
+ message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
}
if len(updateFiles) != 0 {
- message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
+ message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
}
if len(deleteFiles) != 0 {
- message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
+ message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
}
return strings.Trim(message, "\n")
}
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 69433bf4cc..a1e3c9804b 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -14,11 +14,11 @@ import (
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"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
if err != nil {
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
ctx.Error(http.StatusConflict, "ForkRepository", err)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "ForkRepository", err)
} else {
ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
}
diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go
index 7e471e263b..26ae84d08d 100644
--- a/routers/api/v1/repo/git_hook.go
+++ b/routers/api/v1/repo/git_hook.go
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go
index 34d2dcfcc8..0fa58425b8 100644
--- a/routers/api/v1/repo/git_ref.go
+++ b/routers/api/v1/repo/git_ref.go
@@ -7,10 +7,10 @@ import (
"net/http"
"net/url"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
)
// GetGitAllRefs get ref or an list all the refs of a repository
diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go
index 8859e3ae23..ffd2313591 100644
--- a/routers/api/v1/repo/hook.go
+++ b/routers/api/v1/repo/hook.go
@@ -11,13 +11,13 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
webhook_service "code.gitea.io/gitea/services/webhook"
)
diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go
index 94a71e20ad..37cf61c1ed 100644
--- a/routers/api/v1/repo/hook_test.go
+++ b/routers/api/v1/repo/hook_test.go
@@ -9,7 +9,7 @@ import (
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/contexttest"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 0f76a4b4ff..5e173abf88 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -5,6 +5,7 @@
package repo
import (
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -18,14 +19,14 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
@@ -122,14 +123,14 @@ func SearchIssues(ctx *context.APIContext) {
return
}
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
- isClosed = util.OptionalBoolTrue
+ isClosed = optional.Some(true)
case "all":
- isClosed = util.OptionalBoolNone
+ isClosed = optional.None[bool]()
default:
- isClosed = util.OptionalBoolFalse
+ isClosed = optional.Some(false)
}
var (
@@ -142,7 +143,7 @@ func SearchIssues(ctx *context.APIContext) {
Private: false,
AllPublic: true,
TopicOnly: false,
- Collaborate: util.OptionalBoolNone,
+ Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
@@ -165,7 +166,7 @@ func SearchIssues(ctx *context.APIContext) {
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
- opts.Collaborate = util.OptionalBoolFalse
+ opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
@@ -204,14 +205,14 @@ func SearchIssues(ctx *context.APIContext) {
keyword = ""
}
- var isPull util.OptionalBool
+ var isPull optional.Option[bool]
switch ctx.FormString("type") {
case "pulls":
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
case "issues":
- isPull = util.OptionalBoolFalse
+ isPull = optional.Some(false)
default:
- isPull = util.OptionalBoolNone
+ isPull = optional.None[bool]()
}
var includedAnyLabels []int64
@@ -268,28 +269,28 @@ func SearchIssues(ctx *context.APIContext) {
}
if since != 0 {
- searchOpt.UpdatedAfterUnix = &since
+ searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
- searchOpt.UpdatedBeforeUnix = &before
+ searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
- searchOpt.PosterID = &ctxUserID
+ searchOpt.PosterID = optional.Some(ctxUserID)
}
if ctx.FormBool("assigned") {
- searchOpt.AssigneeID = &ctxUserID
+ searchOpt.AssigneeID = optional.Some(ctxUserID)
}
if ctx.FormBool("mentioned") {
- searchOpt.MentionID = &ctxUserID
+ searchOpt.MentionID = optional.Some(ctxUserID)
}
if ctx.FormBool("review_requested") {
- searchOpt.ReviewRequestedID = &ctxUserID
+ searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
}
if ctx.FormBool("reviewed") {
- searchOpt.ReviewedID = &ctxUserID
+ searchOpt.ReviewedID = optional.Some(ctxUserID)
}
}
@@ -310,7 +311,7 @@ func SearchIssues(ctx *context.APIContext) {
ctx.SetLinkHeader(int(total), limit)
ctx.SetTotalCountHeader(total)
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
// ListIssues list the issues of a repository
@@ -367,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
// required: false
// - name: created_by
// in: query
- // description: Only show items which were created by the the given user
+ // description: Only show items which were created by the given user
// type: string
// - name: assigned_by
// in: query
@@ -396,14 +397,14 @@ func ListIssues(ctx *context.APIContext) {
return
}
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
- isClosed = util.OptionalBoolTrue
+ isClosed = optional.Some(true)
case "all":
- isClosed = util.OptionalBoolNone
+ isClosed = optional.None[bool]()
default:
- isClosed = util.OptionalBoolFalse
+ isClosed = optional.Some(false)
}
keyword := ctx.FormTrim("q")
@@ -452,31 +453,29 @@ func ListIssues(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
- var isPull util.OptionalBool
+ isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
case "issues":
- isPull = util.OptionalBoolFalse
- default:
- isPull = util.OptionalBoolNone
+ isPull = optional.Some(false)
}
- if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) {
+ if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
ctx.NotFound()
return
}
- if isPull == util.OptionalBoolNone {
+ if !isPull.Has() {
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
if !canReadIssues && !canReadPulls {
ctx.NotFound()
return
} else if !canReadIssues {
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
} else if !canReadPulls {
- isPull = util.OptionalBoolFalse
+ isPull = optional.Some(false)
}
}
@@ -503,10 +502,10 @@ func ListIssues(ctx *context.APIContext) {
SortBy: issue_indexer.SortByCreatedDesc,
}
if since != 0 {
- searchOpt.UpdatedAfterUnix = &since
+ searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
- searchOpt.UpdatedBeforeUnix = &before
+ searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if len(labelIDs) == 1 && labelIDs[0] == 0 {
searchOpt.NoLabelOnly = true
@@ -527,13 +526,13 @@ func ListIssues(ctx *context.APIContext) {
}
if createdByID > 0 {
- searchOpt.PosterID = &createdByID
+ searchOpt.PosterID = optional.Some(createdByID)
}
if assignedByID > 0 {
- searchOpt.AssigneeID = &assignedByID
+ searchOpt.AssigneeID = optional.Some(assignedByID)
}
if mentionedByID > 0 {
- searchOpt.MentionID = &mentionedByID
+ searchOpt.MentionID = optional.Some(mentionedByID)
}
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
@@ -549,7 +548,7 @@ func ListIssues(ctx *context.APIContext) {
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetTotalCountHeader(total)
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
@@ -615,7 +614,7 @@ func GetIssue(ctx *context.APIContext) {
ctx.NotFound()
return
}
- ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}
// CreateIssue create an issue of a repository
@@ -655,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
+
form := web.GetForm(ctx).(*api.CreateIssueOption)
var deadlineUnix timeutil.TimeStamp
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
@@ -709,12 +709,14 @@ func CreateIssue(ctx *context.APIContext) {
form.Labels = make([]int64, 0)
}
- if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
+ if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "NewIssue", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "NewIssue", err)
}
- ctx.Error(http.StatusInternalServerError, "NewIssue", err)
return
}
@@ -735,7 +737,7 @@ func CreateIssue(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}
// EditIssue modify an issue of a repository
@@ -850,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
+ }
return
}
}
@@ -866,10 +872,11 @@ func EditIssue(ctx *context.APIContext) {
}
if form.State != nil {
if issue.IsPull {
- if pr, err := issue.GetPullRequest(ctx); err != nil {
+ if err := issue.LoadPullRequest(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
return
- } else if pr.HasMerged {
+ }
+ if issue.PullRequest.HasMerged {
ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
return
}
@@ -904,7 +911,7 @@ func EditIssue(ctx *context.APIContext) {
ctx.InternalServerError(err)
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}
func DeleteIssue(ctx *context.APIContext) {
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
index 11d19b21ff..7a5c6d554d 100644
--- a/routers/api/v1/repo/issue_attachment.go
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -8,12 +8,12 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -107,7 +107,7 @@ func ListIssueAttachments(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue).Attachments)
+ ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue).Attachments)
}
// CreateIssueAttachment creates an attachment and saves the given file
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 4db2c68a79..070571ba62 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -14,11 +14,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -278,15 +278,15 @@ func ListRepoIssueComments(ctx *context.APIContext) {
return
}
- var isPull util.OptionalBool
+ var isPull optional.Option[bool]
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
if canReadIssue && canReadPull {
- isPull = util.OptionalBoolNone
+ isPull = optional.None[bool]()
} else if canReadIssue {
- isPull = util.OptionalBoolFalse
+ isPull = optional.Some(false)
} else if canReadPull {
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
} else {
ctx.NotFound()
return
@@ -323,10 +323,6 @@ func ListRepoIssueComments(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
return
}
- if err := comments.LoadPosters(ctx); err != nil {
- ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
- return
- }
if err := comments.LoadAttachments(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
return
@@ -382,6 +378,7 @@ func CreateIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
+
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
@@ -395,13 +392,17 @@ func CreateIssueComment(ctx *context.APIContext) {
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
- ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
+ ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
return
}
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
+ }
return
}
@@ -522,6 +523,7 @@ func EditIssueComment(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
+
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
editIssueComment(ctx, *form)
}
@@ -610,7 +612,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
oldContent := comment.Content
comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
- ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateComment", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
+ }
return
}
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
index 21e2f4dabd..4096cbf07b 100644
--- a/routers/api/v1/repo/issue_comment_attachment.go
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -4,16 +4,18 @@
package repo
import (
+ "errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/Attachment"
// "400":
// "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/error"
// "423":
@@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
- ctx.ServerError("UpdateComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateComment", err)
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
return
}
diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go
index 62d1057cdf..c40e92c01b 100644
--- a/routers/api/v1/repo/issue_dependency.go
+++ b/routers/api/v1/repo/issue_dependency.go
@@ -11,10 +11,10 @@ import (
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"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -153,7 +153,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
blockerIssues = append(blockerIssues, &blocker.Issue)
}
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, blockerIssues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
}
// CreateIssueDependency create a new issue dependencies
@@ -214,7 +214,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
}
// RemoveIssueDependency remove an issue dependency
@@ -275,7 +275,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
}
// GetIssueBlocks list issues that are blocked by this issue
@@ -381,7 +381,7 @@ func GetIssueBlocks(ctx *context.APIContext) {
issues = append(issues, &depMeta.Issue)
}
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
// CreateIssueBlocking block the issue given in the body by the issue in path
@@ -438,7 +438,7 @@ func CreateIssueBlocking(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
}
// RemoveIssueBlocking unblock the issue given in the body by the issue in path
@@ -495,7 +495,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
}
func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index c2f530956e..7d9f85d2aa 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -8,9 +8,9 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go
index 61f88de34e..af3e06332a 100644
--- a/routers/api/v1/repo/issue_pin.go
+++ b/routers/api/v1/repo/issue_pin.go
@@ -7,8 +7,8 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -207,7 +207,7 @@ func ListPinnedIssues(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
// ListPinnedPullRequests returns a list of all pinned PRs
@@ -240,18 +240,12 @@ func ListPinnedPullRequests(ctx *context.APIContext) {
}
apiPrs := make([]*api.PullRequest, len(issues))
+ if err := issues.LoadPullRequests(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadPullRequests", err)
+ return
+ }
for i, currentIssue := range issues {
- pr, err := currentIssue.GetPullRequest(ctx)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
- return
- }
-
- if err = pr.LoadIssue(ctx); err != nil {
- ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
- return
- }
-
+ pr := currentIssue.PullRequest
if err = pr.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
index c886bd71b7..3ff3d19f13 100644
--- a/routers/api/v1/repo/issue_reaction.go
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -8,11 +8,13 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
+ user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
+ issue_service "code.gitea.io/gitea/services/issue"
)
// GetIssueCommentReactions list reactions of a comment from an issue
@@ -218,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
if isCreateType {
// PostIssueCommentReaction part
- reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
+ reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, err.Error(), err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
@@ -434,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
if isCreateType {
// PostIssueReaction part
- reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
+ reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.Error(http.StatusForbidden, err.Error(), err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
@@ -445,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
Created: reaction.CreatedUnix.AsTime(),
})
} else {
- ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
+ ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
}
return
}
diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go
index 52bf8b5c7b..d9054e8f77 100644
--- a/routers/api/v1/repo/issue_stopwatch.go
+++ b/routers/api/v1/repo/issue_stopwatch.go
@@ -8,8 +8,8 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go
index ece880c03e..a535172462 100644
--- a/routers/api/v1/repo/issue_subscription.go
+++ b/routers/api/v1/repo/issue_subscription.go
@@ -9,9 +9,9 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index cf03e72aa0..f83855efac 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -12,10 +12,10 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -138,7 +138,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(count)
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
// AddTime add time manual to the given issue
@@ -225,7 +225,7 @@ func AddTime(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
- ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, trackedTime))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, user, trackedTime))
}
// ResetIssueTime reset time manual to the given issue
@@ -455,7 +455,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
// ListTrackedTimesByRepository lists all tracked times of the repository
@@ -567,7 +567,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(count)
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
// ListMyTrackedTimes lists all tracked times of the current user
@@ -629,5 +629,5 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(count)
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go
index af48c40885..88444a2625 100644
--- a/routers/api/v1/repo/key.go
+++ b/routers/api/v1/repo/key.go
@@ -15,12 +15,12 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go
index 420d3ab5b4..b6eb51fd20 100644
--- a/routers/api/v1/repo/label.go
+++ b/routers/api/v1/repo/label.go
@@ -9,11 +9,11 @@ import (
"strconv"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/language.go b/routers/api/v1/repo/language.go
index 12f1761ad0..f1d5bbe45f 100644
--- a/routers/api/v1/repo/language.go
+++ b/routers/api/v1/repo/language.go
@@ -9,8 +9,8 @@ import (
"strconv"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
type languageResponse []*repo_model.LanguageStat
diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go
index 839fbfe8a1..2caaa130e8 100644
--- a/routers/api/v1/repo/migrate.go
+++ b/routers/api/v1/repo/migrate.go
@@ -17,7 +17,6 @@ import (
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"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -26,6 +25,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index 9c2ed16d93..b9534016e4 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -11,12 +11,12 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -61,10 +61,10 @@ func ListMilestones(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
state := api.StateType(ctx.FormString("state"))
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch state {
case api.StateClosed, api.StateOpen:
- isClosed = util.OptionalBoolOf(state == api.StateClosed)
+ isClosed = optional.Some(state == api.StateClosed)
}
milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go
index 26e0be301c..864644e1ef 100644
--- a/routers/api/v1/repo/mirror.go
+++ b/routers/api/v1/repo/mirror.go
@@ -13,12 +13,12 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go
index e7e00dae41..a4a1d4eab7 100644
--- a/routers/api/v1/repo/notes.go
+++ b/routers/api/v1/repo/notes.go
@@ -7,9 +7,9 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
index 9b5635d245..0e0601b7d9 100644
--- a/routers/api/v1/repo/patch.go
+++ b/routers/api/v1/repo/patch.go
@@ -10,10 +10,10 @@ import (
"code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/repository/files"
)
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index eaf406e64d..e43366ff14 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -21,7 +21,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
@@ -32,6 +32,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff"
@@ -96,13 +97,17 @@ func ListPullRequests(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
+ labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "PullRequests", err)
+ return
+ }
listOptions := utils.GetListOptions(ctx)
-
prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
ListOptions: listOptions,
State: ctx.FormTrim("state"),
SortType: ctx.FormTrim("sort"),
- Labels: ctx.FormStrings("labels"),
+ Labels: labelIDs,
MilestoneID: ctx.FormInt64("milestone"),
})
if err != nil {
@@ -187,6 +192,91 @@ func GetPullRequest(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
}
+// GetPullRequest returns a single PR based on index
+func GetPullRequestByBaseHead(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/pulls/{base}/{head} repository repoGetPullRequestByBaseHead
+ // ---
+ // summary: Get a pull request by base and head
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: base
+ // in: path
+ // description: base of the pull request to get
+ // type: string
+ // required: true
+ // - name: head
+ // in: path
+ // description: head of the pull request to get
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/PullRequest"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ var headRepoID int64
+ var headBranch string
+ head := ctx.Params("*")
+ if strings.Contains(head, ":") {
+ split := strings.SplitN(head, ":", 2)
+ headBranch = split[1]
+ var owner, name string
+ if strings.Contains(split[0], "/") {
+ split = strings.Split(split[0], "/")
+ owner = split[0]
+ name = split[1]
+ } else {
+ owner = split[0]
+ name = ctx.Repo.Repository.Name
+ }
+ repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name)
+ if err != nil {
+ if repo_model.IsErrRepoNotExist(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerName", err)
+ }
+ return
+ }
+ headRepoID = repo.ID
+ } else {
+ headRepoID = ctx.Repo.Repository.ID
+ headBranch = head
+ }
+
+ pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.Params(":base"), headBranch)
+ if err != nil {
+ if issues_model.IsErrPullRequestNotExist(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetPullRequestByBaseHeadInfo", err)
+ }
+ return
+ }
+
+ if err = pr.LoadBaseRepo(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
+ return
+ }
+ if err = pr.LoadHeadRepo(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
+}
+
// DownloadPullDiffOrPatch render a pull's raw diff or patch
func DownloadPullDiffOrPatch(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.{diffType} repository repoDownloadPullDiffOrPatch
@@ -277,6 +367,8 @@ func CreatePullRequest(ctx *context.APIContext) {
// responses:
// "201":
// "$ref": "#/responses/PullRequest"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
@@ -425,9 +517,11 @@ func CreatePullRequest(ctx *context.APIContext) {
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
}
- ctx.Error(http.StatusInternalServerError, "NewPullRequest", err)
return
}
@@ -545,6 +639,8 @@ func EditPullRequest(ctx *context.APIContext) {
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
} else {
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
}
@@ -977,6 +1073,8 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil, nil, nil, "", ""
}
headBranch = headInfos[1]
+ // The head repository can also point to the same repo
+ isSameRepo = ctx.Repo.Owner.ID == headUser.ID
} else {
ctx.NotFound()
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 07d8f4877b..17bb2085b6 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -12,11 +12,11 @@ import (
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
@@ -362,6 +362,7 @@ func CreatePullReview(ctx *context.APIContext) {
true, // pending review
0, // no reply
opts.CommitID,
+ nil,
); err != nil {
ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err)
return
@@ -544,7 +545,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues
return nil, nil, true
}
- // validate the the review is for the given PR
+ // validate the review is for the given PR
if review.IssueID != pr.IssueID {
ctx.NotFound("ReviewNotInPR")
return nil, nil, true
@@ -639,6 +640,8 @@ func DeleteReviewRequests(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "422":
// "$ref": "#/responses/validationError"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
opts := web.GetForm(ctx).(*api.PullReviewRequestOptions)
@@ -707,6 +710,10 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
for _, reviewer := range reviewers {
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd)
if err != nil {
+ if issues_model.IsErrReviewRequestOnClosedPR(err) {
+ ctx.Error(http.StatusForbidden, "", err)
+ return
+ }
ctx.Error(http.StatusInternalServerError, "ReviewRequest", err)
return
}
@@ -873,7 +880,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
return
}
- review, pr, isWrong := prepareSingleReview(ctx)
+ review, _, isWrong := prepareSingleReview(ctx)
if isWrong {
return
}
@@ -883,13 +890,12 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
return
}
- if pr.Issue.IsClosed {
- ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because this pr is closed")
- return
- }
-
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
if err != nil {
+ if pull_service.IsErrDismissRequestOnClosedPR(err) {
+ ctx.Error(http.StatusForbidden, "", err)
+ return
+ }
ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
return
}
diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go
index a41c5ba7d8..f0f3c0bbc7 100644
--- a/routers/api/v1/repo/release.go
+++ b/routers/api/v1/repo/release.go
@@ -4,6 +4,7 @@
package repo
import (
+ "fmt"
"net/http"
"code.gitea.io/gitea/models"
@@ -11,10 +12,10 @@ import (
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
)
@@ -215,6 +216,10 @@ func CreateRelease(ctx *context.APIContext) {
// "409":
// "$ref": "#/responses/error"
form := web.GetForm(ctx).(*api.CreateReleaseOption)
+ if ctx.Repo.Repository.IsEmpty {
+ ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
+ return
+ }
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
if err != nil {
if !repo_model.IsErrReleaseNotExist(err) {
diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index c36bf12e6d..59fd83e3a2 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -4,16 +4,18 @@
package repo
import (
+ "io"
"net/http"
+ "strings"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
)
@@ -154,6 +156,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
// - application/json
// consumes:
// - multipart/form-data
+ // - application/octet-stream
// parameters:
// - name: owner
// in: path
@@ -180,7 +183,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
// in: formData
// description: attachment to upload
// type: file
- // required: true
+ // required: false
// responses:
// "201":
// "$ref": "#/responses/Attachment"
@@ -202,20 +205,36 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
}
// Get uploaded file from request
- file, header, err := ctx.Req.FormFile("attachment")
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetFile", err)
- return
- }
- defer file.Close()
+ var content io.ReadCloser
+ var filename string
+ var size int64 = -1
- filename := header.Filename
- if query := ctx.FormString("name"); query != "" {
- filename = query
+ if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
+ file, header, err := ctx.Req.FormFile("attachment")
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetFile", err)
+ return
+ }
+ defer file.Close()
+
+ content = file
+ size = header.Size
+ filename = header.Filename
+ if name := ctx.FormString("name"); name != "" {
+ filename = name
+ }
+ } else {
+ content = ctx.Req.Body
+ filename = ctx.FormString("name")
+ }
+
+ if filename == "" {
+ ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.")
+ return
}
// Create a new attachment and save the file
- attach, err := attachment.UploadAttachment(ctx, file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{
+ attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go
index 9f2098df06..fec91164a2 100644
--- a/routers/api/v1/repo/release_tags.go
+++ b/routers/api/v1/repo/release_tags.go
@@ -8,7 +8,7 @@ import (
"code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
releaseservice "code.gitea.io/gitea/services/release"
)
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 2efdccb569..822e368fa8 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -8,9 +8,11 @@ import (
"fmt"
"net/http"
"slices"
+ "strconv"
"strings"
"time"
+ actions_model "code.gitea.io/gitea/models/actions"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
@@ -19,18 +21,19 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/issue"
repo_service "code.gitea.io/gitea/services/repository"
@@ -134,33 +137,33 @@ func Search(ctx *context.APIContext) {
PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
TeamID: ctx.FormInt64("team_id"),
TopicOnly: ctx.FormBool("topic"),
- Collaborate: util.OptionalBoolNone,
+ Collaborate: optional.None[bool](),
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
- Template: util.OptionalBoolNone,
+ Template: optional.None[bool](),
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
}
if ctx.FormString("template") != "" {
- opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
+ opts.Template = optional.Some(ctx.FormBool("template"))
}
if ctx.FormBool("exclusive") {
- opts.Collaborate = util.OptionalBoolFalse
+ opts.Collaborate = optional.Some(false)
}
mode := ctx.FormString("mode")
switch mode {
case "source":
- opts.Fork = util.OptionalBoolFalse
- opts.Mirror = util.OptionalBoolFalse
+ opts.Fork = optional.Some(false)
+ opts.Mirror = optional.Some(false)
case "fork":
- opts.Fork = util.OptionalBoolTrue
+ opts.Fork = optional.Some(true)
case "mirror":
- opts.Mirror = util.OptionalBoolTrue
+ opts.Mirror = optional.Some(true)
case "collaborative":
- opts.Mirror = util.OptionalBoolFalse
- opts.Collaborate = util.OptionalBoolTrue
+ opts.Mirror = optional.Some(false)
+ opts.Collaborate = optional.Some(true)
case "":
default:
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode))
@@ -168,11 +171,11 @@ func Search(ctx *context.APIContext) {
}
if ctx.FormString("archived") != "" {
- opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
+ opts.Archived = optional.Some(ctx.FormBool("archived"))
}
if ctx.FormString("is_private") != "" {
- opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
+ opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
}
sortMode := ctx.FormString("sort")
@@ -357,7 +360,7 @@ func Generate(ctx *context.APIContext) {
return
}
- opts := repo_module.GenerateRepoOptions{
+ opts := repo_service.GenerateRepoOptions{
Name: form.Name,
DefaultBranch: form.DefaultBranch,
Description: form.Description,
@@ -719,7 +722,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
var err error
- ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
+ ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo)
if err != nil {
ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
return err
@@ -730,7 +733,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
// Default branch only updated if changed and exist or the repository is empty
if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
if !repo.IsEmpty {
- if err := ctx.Repo.GitRepo.SetDefaultBranch(*opts.DefaultBranch); err != nil {
+ if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err)
return err
@@ -884,6 +887,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
+ AllowFastForwardOnly: true,
AllowManualMerge: true,
AutodetectManualMerge: false,
AllowRebaseUpdate: true,
@@ -910,6 +914,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if opts.AllowSquash != nil {
config.AllowSquash = *opts.AllowSquash
}
+ if opts.AllowFastForwardOnly != nil {
+ config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
+ }
if opts.AllowManualMerge != nil {
config.AllowManualMerge = *opts.AllowManualMerge
}
@@ -939,13 +946,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
}
}
- if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
- if *opts.HasProjects {
+ currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
+ newHasProjects := currHasProjects
+ if opts.HasProjects != nil {
+ newHasProjects = *opts.HasProjects
+ }
+ if currHasProjects || newHasProjects {
+ if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
+ unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
+ var config *repo_model.ProjectsConfig
+ if err != nil {
+ config = &repo_model.ProjectsConfig{
+ ProjectsMode: repo_model.ProjectsModeAll,
+ }
+ } else {
+ config = unit.ProjectsConfig()
+ }
+
+ if opts.ProjectsMode != nil {
+ config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
+ }
+
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypeProjects,
+ Config: config,
})
- } else {
+ } else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
}
}
@@ -1010,6 +1037,9 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
return err
}
+ if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+ log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
} else {
if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil {
@@ -1017,6 +1047,11 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
return err
}
+ if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+ if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+ log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
+ }
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
}
}
@@ -1161,12 +1196,11 @@ func GetIssueTemplates(ctx *context.APIContext) {
// "$ref": "#/responses/IssueTemplates"
// "404":
// "$ref": "#/responses/notFound"
- ret, err := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetTemplatesFromDefaultBranch", err)
- return
+ ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+ if cnt := len(ret.TemplateErrors); cnt != 0 {
+ ctx.Resp.Header().Add("X-Gitea-Warning", "error occurs when parsing issue template: count="+strconv.Itoa(cnt))
}
- ctx.JSON(http.StatusOK, ret)
+ ctx.JSON(http.StatusOK, ret.IssueTemplates)
}
// GetIssueConfig returns the issue config for a repo
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
index 29e2d1f21d..8d6ca9e3b5 100644
--- a/routers/api/v1/repo/repo_test.go
+++ b/routers/api/v1/repo/repo_test.go
@@ -9,9 +9,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -35,6 +35,7 @@ func TestRepoEdit(t *testing.T) {
allowRebase := false
allowRebaseMerge := false
allowSquashMerge := false
+ allowFastForwardOnlyMerge := false
archived := true
opts := api.EditRepoOption{
Name: &ctx.Repo.Repository.Name,
@@ -50,6 +51,7 @@ func TestRepoEdit(t *testing.T) {
AllowRebase: &allowRebase,
AllowRebaseMerge: &allowRebaseMerge,
AllowSquash: &allowSquashMerge,
+ AllowFastForwardOnly: &allowFastForwardOnlyMerge,
Archived: &archived,
}
diff --git a/routers/api/v1/repo/runners.go b/routers/api/v1/repo/runners.go
index 0a2bbf8117..fe133b311d 100644
--- a/routers/api/v1/repo/runners.go
+++ b/routers/api/v1/repo/runners.go
@@ -4,8 +4,8 @@
package repo
import (
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
)
// GetRegistrationToken returns the token to register repo runners
diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go
index 05227e33a0..99676de119 100644
--- a/routers/api/v1/repo/star.go
+++ b/routers/api/v1/repo/star.go
@@ -7,9 +7,9 @@ import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go
index b4edf0608c..9e36ea0aed 100644
--- a/routers/api/v1/repo/status.go
+++ b/routers/api/v1/repo/status.go
@@ -9,12 +9,12 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
- files_service "code.gitea.io/gitea/services/repository/files"
+ commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
)
// NewCommitStatus creates a new CommitStatus
@@ -64,7 +64,7 @@ func NewCommitStatus(ctx *context.APIContext) {
Description: form.Description,
Context: form.Context,
}
- if err := files_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
+ if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
ctx.Error(http.StatusInternalServerError, "CreateCommitStatus", err)
return
}
diff --git a/routers/api/v1/repo/subscriber.go b/routers/api/v1/repo/subscriber.go
index 05509fc443..8584182857 100644
--- a/routers/api/v1/repo/subscriber.go
+++ b/routers/api/v1/repo/subscriber.go
@@ -7,9 +7,9 @@ import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go
index 2f19f95e66..a6908f3615 100644
--- a/routers/api/v1/repo/tag.go
+++ b/routers/api/v1/repo/tag.go
@@ -10,10 +10,10 @@ import (
"code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
releaseservice "code.gitea.io/gitea/services/release"
)
diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go
index 1bacc71211..0ecf3a39d8 100644
--- a/routers/api/v1/repo/teams.go
+++ b/routers/api/v1/repo/teams.go
@@ -8,7 +8,7 @@ import (
"net/http"
"code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go
index d662b9b583..9852caa989 100644
--- a/routers/api/v1/repo/topic.go
+++ b/routers/api/v1/repo/topic.go
@@ -7,12 +7,13 @@ import (
"net/http"
"strings"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -53,7 +54,7 @@ func ListTopics(ctx *context.APIContext) {
RepoID: ctx.Repo.Repository.ID,
}
- topics, total, err := repo_model.FindTopics(ctx, opts)
+ topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -172,7 +173,7 @@ func AddTopic(ctx *context.APIContext) {
}
// Prevent adding more topics than allowed to repo
- count, err := repo_model.CountTopics(ctx, &repo_model.FindTopicOptions{
+ count, err := db.Count[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@@ -287,7 +288,7 @@ func TopicSearch(ctx *context.APIContext) {
ListOptions: utils.GetListOptions(ctx),
}
- topics, total, err := repo_model.FindTopics(ctx, opts)
+ topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go
index c0a40ce062..776b336761 100644
--- a/routers/api/v1/repo/transfer.go
+++ b/routers/api/v1/repo/transfer.go
@@ -4,6 +4,7 @@
package repo
import (
+ "errors"
"fmt"
"net/http"
@@ -13,10 +14,10 @@ import (
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"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -117,7 +118,11 @@ func Transfer(ctx *context.APIContext) {
return
}
- ctx.InternalServerError(err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.InternalServerError(err)
+ }
return
}
diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go
index f63100b6ea..353a996d5b 100644
--- a/routers/api/v1/repo/tree.go
+++ b/routers/api/v1/repo/tree.go
@@ -6,7 +6,7 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
)
diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index 4f27500496..f18ea087c4 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -10,13 +10,13 @@ import (
"net/url"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
notify_service "code.gitea.io/gitea/services/notify"
wiki_service "code.gitea.io/gitea/services/wiki"
diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go
index 02bda1309d..0ee81b96d5 100644
--- a/routers/api/v1/settings/settings.go
+++ b/routers/api/v1/settings/settings.go
@@ -6,9 +6,9 @@ package settings
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
)
// GetGeneralUISettings returns instance's global settings for ui
diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go
new file mode 100644
index 0000000000..a1e65625ed
--- /dev/null
+++ b/routers/api/v1/shared/block.go
@@ -0,0 +1,98 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package shared
+
+import (
+ "errors"
+ "net/http"
+
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+ user_service "code.gitea.io/gitea/services/user"
+)
+
+func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
+ blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ BlockerID: blocker.ID,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindBlockings", err)
+ return
+ }
+
+ if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+ return
+ }
+
+ users := make([]*api.User, 0, len(blocks))
+ for _, b := range blocks {
+ users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
+ }
+
+ ctx.SetTotalCountHeader(total)
+ ctx.JSON(http.StatusOK, &users)
+}
+
+func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
+ blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+ if err != nil {
+ ctx.NotFound("GetUserByName", err)
+ return
+ }
+
+ status := http.StatusNotFound
+ blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetBlocking", err)
+ return
+ }
+ if blocking != nil {
+ status = http.StatusNoContent
+ }
+
+ ctx.Status(status)
+}
+
+func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
+ blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+ if err != nil {
+ ctx.NotFound("GetUserByName", err)
+ return
+ }
+
+ if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
+ if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Error(http.StatusBadRequest, "BlockUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "BlockUser", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
+ blockee, err := user_model.GetUserByName(ctx, ctx.Params("username"))
+ if err != nil {
+ ctx.NotFound("GetUserByName", err)
+ return
+ }
+
+ if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
+ if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Error(http.StatusBadRequest, "UnblockUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UnblockUser", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go
index a342bd4b63..c850ad7866 100644
--- a/routers/api/v1/shared/runners.go
+++ b/routers/api/v1/shared/runners.go
@@ -8,8 +8,8 @@ import (
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
// RegistrationToken is response related to registeration token
diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go
index 3771780718..665f4d0b85 100644
--- a/routers/api/v1/swagger/action.go
+++ b/routers/api/v1/swagger/action.go
@@ -18,3 +18,17 @@ type swaggerResponseSecret struct {
// in:body
Body api.Secret `json:"body"`
}
+
+// ActionVariable
+// swagger:response ActionVariable
+type swaggerResponseActionVariable struct {
+ // in:body
+ Body api.ActionVariable `json:"body"`
+}
+
+// VariableList
+// swagger:response VariableList
+type swaggerResponseVariableList struct {
+ // in:body
+ Body []api.ActionVariable `json:"body"`
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 6f7859df62..cd551cbdfa 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -190,4 +190,13 @@ type swaggerParameterBodies struct {
// in:body
CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption
+
+ // in:body
+ UserBadgeOption api.UserBadgeOption
+
+ // in:body
+ CreateVariableOption api.CreateVariableOption
+
+ // in:body
+ UpdateVariableOption api.UpdateVariableOption
}
diff --git a/routers/api/v1/swagger/user.go b/routers/api/v1/swagger/user.go
index fb6d185ee7..e2ad511d2b 100644
--- a/routers/api/v1/swagger/user.go
+++ b/routers/api/v1/swagger/user.go
@@ -48,3 +48,10 @@ type swaggerResponseUserSettings struct {
// in:body
Body []api.UserSettings `json:"body"`
}
+
+// BadgeList
+// swagger:response BadgeList
+type swaggerResponseBadgeList struct {
+ // in:body
+ Body []api.Badge `json:"body"`
+}
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index cbe332a779..bf78c2c864 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -7,10 +7,14 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/modules/context"
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
+ "code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -101,3 +105,249 @@ func DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+
+// CreateVariable create a user-level variable
+func CreateVariable(ctx *context.APIContext) {
+ // swagger:operation POST /user/actions/variables/{variablename} user createUserVariable
+ // ---
+ // summary: Create a user-level variable
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateVariableOption"
+ // responses:
+ // "201":
+ // description: response when creating a variable
+ // "204":
+ // description: response when creating a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+ ownerID := ctx.Doer.ID
+ variableName := ctx.Params("variablename")
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ownerID,
+ Name: variableName,
+ })
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ return
+ }
+ if v != nil && v.ID > 0 {
+ ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+ return
+ }
+
+ if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a user-level variable which is created by current doer
+func UpdateVariable(ctx *context.APIContext) {
+ // swagger:operation PUT /user/actions/variables/{variablename} user updateUserVariable
+ // ---
+ // summary: Update a user-level variable which is created by current doer
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UpdateVariableOption"
+ // responses:
+ // "201":
+ // description: response when updating a variable
+ // "204":
+ // description: response when updating a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Doer.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ if opt.Name == "" {
+ opt.Name = ctx.Params("variablename")
+ }
+ if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// DeleteVariable delete a user-level variable which is created by current doer
+func DeleteVariable(ctx *context.APIContext) {
+ // swagger:operation DELETE /user/actions/variables/{variablename} user deleteUserVariable
+ // ---
+ // summary: Delete a user-level variable which is created by current doer
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "201":
+ // description: response when deleting a variable
+ // "204":
+ // description: response when deleting a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// GetVariable get a user-level variable which is created by current doer
+func GetVariable(ctx *context.APIContext) {
+ // swagger:operation GET /user/actions/variables/{variablename} user getUserVariable
+ // ---
+ // summary: Get a user-level variable which is created by current doer
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Doer.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ variable := &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+
+ ctx.JSON(http.StatusOK, variable)
+}
+
+// ListVariables list user-level variables
+func ListVariables(ctx *context.APIContext) {
+ // swagger:operation GET /user/actions/variables user getUserVariablesList
+ // ---
+ // summary: Get the user-level list of variables which is created by current doer
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/VariableList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+ OwnerID: ctx.Doer.ID,
+ ListOptions: utils.GetListOptions(ctx),
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+ return
+ }
+
+ variables := make([]*api.ActionVariable, len(vars))
+ for i, v := range vars {
+ variables[i] = &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+ }
+
+ ctx.SetTotalCountHeader(count)
+ ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go
index f045fb4d5d..88e314ed31 100644
--- a/routers/api/v1/user/app.go
+++ b/routers/api/v1/user/app.go
@@ -13,10 +13,10 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go
index 1c1bb6181a..f912296228 100644
--- a/routers/api/v1/user/avatar.go
+++ b/routers/api/v1/user/avatar.go
@@ -7,9 +7,9 @@ import (
"encoding/base64"
"net/http"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go
new file mode 100644
index 0000000000..7231e9add7
--- /dev/null
+++ b/routers/api/v1/user/block.go
@@ -0,0 +1,96 @@
+// Copyright 2024 The Gitea Authors.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
+)
+
+func ListBlocks(ctx *context.APIContext) {
+ // swagger:operation GET /user/blocks user userListBlocks
+ // ---
+ // summary: List users blocked by the authenticated user
+ // parameters:
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // produces:
+ // - application/json
+ // responses:
+ // "200":
+ // "$ref": "#/responses/UserList"
+
+ shared.ListBlocks(ctx, ctx.Doer)
+}
+
+func CheckUserBlock(ctx *context.APIContext) {
+ // swagger:operation GET /user/blocks/{username} user userCheckUserBlock
+ // ---
+ // summary: Check if a user is blocked by the authenticated user
+ // parameters:
+ // - name: username
+ // in: path
+ // description: user to check
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ shared.CheckUserBlock(ctx, ctx.Doer)
+}
+
+func BlockUser(ctx *context.APIContext) {
+ // swagger:operation PUT /user/blocks/{username} user userBlockUser
+ // ---
+ // summary: Block a user
+ // parameters:
+ // - name: username
+ // in: path
+ // description: user to block
+ // type: string
+ // required: true
+ // - name: note
+ // in: query
+ // description: optional note for the block
+ // type: string
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.BlockUser(ctx, ctx.Doer)
+}
+
+func UnblockUser(ctx *context.APIContext) {
+ // swagger:operation DELETE /user/blocks/{username} user userUnblockUser
+ // ---
+ // summary: Unblock a user
+ // parameters:
+ // - name: username
+ // in: path
+ // description: user to unblock
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ shared.UnblockUser(ctx, ctx.Doer, ctx.Doer)
+}
diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go
index 3dcea9083c..33aa851a80 100644
--- a/routers/api/v1/user/email.go
+++ b/routers/api/v1/user/email.go
@@ -8,9 +8,9 @@ import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
user_service "code.gitea.io/gitea/services/user"
)
diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go
index 5815ed4f0b..6abb70de19 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -5,12 +5,13 @@
package user
import (
+ "errors"
"net/http"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -221,11 +222,17 @@ func Follow(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
- if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
- ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+ if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "FollowUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "FollowUser", err)
+ }
return
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index 234da5dfdc..5a2f995e1b 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -10,10 +10,12 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -132,6 +134,11 @@ func GetGPGKey(ctx *context.APIContext) {
// CreateUserGPGKey creates new GPG key to given user by ID.
func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ return
+ }
+
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
@@ -268,6 +275,11 @@ func DeleteGPGKey(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ return
+ }
+
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.ParamsInt64(":id")); err != nil {
if asymkey_model.IsErrGPGKeyAccessDenied(err) {
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
index 392b266ebd..8b5c64e291 100644
--- a/routers/api/v1/user/helper.go
+++ b/routers/api/v1/user/helper.go
@@ -7,7 +7,7 @@ import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
// GetUserByParamsName get user by name
diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go
index e87385e4a2..9d9ca5bf01 100644
--- a/routers/api/v1/user/hook.go
+++ b/routers/api/v1/user/hook.go
@@ -6,10 +6,10 @@ package user
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
)
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index dd185aa7d6..d9456e7ec6 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -5,19 +5,20 @@ package user
import (
std_ctx "context"
+ "fmt"
"net/http"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -198,6 +199,11 @@ func GetPublicKey(ctx *context.APIContext) {
// CreateUserPublicKey creates new public key to given user by ID.
func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) {
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ return
+ }
+
content, err := asymkey_model.CheckPublicKeyString(form.Key)
if err != nil {
repo.HandleCheckKeyStringError(ctx, err)
@@ -263,6 +269,11 @@ func DeletePublicKey(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ return
+ }
+
id := ctx.ParamsInt64(":id")
externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id)
if err != nil {
diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go
index b8b2d265bf..81f8e0f3fe 100644
--- a/routers/api/v1/user/repo.go
+++ b/routers/api/v1/user/repo.go
@@ -11,9 +11,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go
index 51556ae0fb..899218473e 100644
--- a/routers/api/v1/user/runners.go
+++ b/routers/api/v1/user/runners.go
@@ -4,8 +4,8 @@
package user
import (
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/shared"
+ "code.gitea.io/gitea/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go
index 062df1ca43..d0a8daaa85 100644
--- a/routers/api/v1/user/settings.go
+++ b/routers/api/v1/user/settings.go
@@ -6,10 +6,10 @@ package user
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
user_service "code.gitea.io/gitea/services/user"
)
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index 2659789ddd..ad9ed9548d 100644
--- a/routers/api/v1/user/star.go
+++ b/routers/api/v1/user/star.go
@@ -5,23 +5,26 @@
package user
import (
- std_context "context"
+ "errors"
"net/http"
- "code.gitea.io/gitea/models/db"
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"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
// getStarredRepos returns the repos that the user with the specified userID has
// starred
-func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
- starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
+func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
+ starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ StarrerID: user.ID,
+ IncludePrivate: private,
+ })
if err != nil {
return nil, err
}
@@ -65,7 +68,7 @@ func GetStarredRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
private := ctx.ContextUser.ID == ctx.Doer.ID
- repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+ repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
return
@@ -95,7 +98,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/RepositoryList"
- repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+ repos, err := getStarredRepos(ctx, ctx.Doer, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
}
@@ -152,12 +155,18 @@ func Star(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "StarRepo", err)
+ }
return
}
ctx.Status(http.StatusNoContent)
@@ -185,7 +194,7 @@ func Unstar(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil {
ctx.Error(http.StatusInternalServerError, "StarRepo", err)
return
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index fb8f67d072..09147cd2ae 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -9,8 +9,8 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go
index 7f531eafaa..2cc23ae476 100644
--- a/routers/api/v1/user/watch.go
+++ b/routers/api/v1/user/watch.go
@@ -4,22 +4,25 @@
package user
import (
- std_context "context"
+ "errors"
"net/http"
- "code.gitea.io/gitea/models/db"
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"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
// getWatchedRepos returns the repos that the user with the specified userID is watching
-func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
- watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions)
+func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
+ watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
+ ListOptions: utils.GetListOptions(ctx),
+ WatcherID: user.ID,
+ IncludePrivate: private,
+ })
if err != nil {
return nil, 0, err
}
@@ -63,7 +66,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
private := ctx.ContextUser.ID == ctx.Doer.ID
- repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
+ repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
}
@@ -92,7 +95,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/RepositoryList"
- repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
+ repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
}
@@ -157,12 +160,18 @@ func Watch(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/WatchInfo"
+ // "403":
+ // "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Error(http.StatusForbidden, "BlockedUser", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
+ }
return
}
ctx.JSON(http.StatusOK, api.WatchInfo{
@@ -197,7 +206,7 @@ func Unwatch(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
return
diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go
index 2299cdc247..4e25137817 100644
--- a/routers/api/v1/utils/git.go
+++ b/routers/api/v1/utils/git.go
@@ -8,10 +8,10 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
// ResolveRefOrSha resolve ref to sha if exist
@@ -72,7 +72,7 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str
// ConvertToObjectID returns a full-length SHA1 from a potential ID string
func ConvertToObjectID(ctx gocontext.Context, repo *context.Repository, commitID string) (git.ObjectID, error) {
- objectFormat, _ := repo.GitRepo.GetObjectFormat()
+ objectFormat := repo.GetObjectFormat()
if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
sha, err := git.NewIDFromString(commitID)
if err == nil {
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 28b21ab8db..f1abd49a7d 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -12,12 +12,12 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
+ "code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
)
diff --git a/routers/api/v1/utils/page.go b/routers/api/v1/utils/page.go
index 6910b82931..024ba7b8d9 100644
--- a/routers/api/v1/utils/page.go
+++ b/routers/api/v1/utils/page.go
@@ -5,7 +5,7 @@ package utils
import (
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/common/auth.go b/routers/common/auth.go
index 8904785d51..115d65ed10 100644
--- a/routers/common/auth.go
+++ b/routers/common/auth.go
@@ -5,9 +5,9 @@ package common
import (
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
auth_service "code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/context"
)
type AuthResult struct {
diff --git a/routers/common/errpage.go b/routers/common/errpage.go
index 923421a29c..402ca44c12 100644
--- a/routers/common/errpage.go
+++ b/routers/common/errpage.go
@@ -9,13 +9,13 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing"
+ "code.gitea.io/gitea/services/context"
)
const tplStatus500 base.TplName = "status/500"
diff --git a/routers/common/markup.go b/routers/common/markup.go
index a1c2c37ac0..2d5638ef61 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -9,11 +9,11 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"mvdan.cc/xurls/v2"
)
@@ -34,7 +34,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
if err := markdown.RenderRaw(&markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
- Base: urlPrefix,
+ AbsolutePrefix: true,
+ Base: urlPrefix,
},
}, strings.NewReader(text), ctx.Resp); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
@@ -79,7 +80,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
if err := markup.Render(&markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
- Base: urlPrefix,
+ AbsolutePrefix: true,
+ Base: urlPrefix,
},
Metas: meta,
IsWiki: wiki,
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
index 8a39dda179..c7c75fb099 100644
--- a/routers/common/middleware.go
+++ b/routers/common/middleware.go
@@ -9,11 +9,11 @@ import (
"strings"
"code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/session"
"github.com/chi-middleware/proxy"
@@ -38,6 +38,7 @@ func ProtocolMiddlewares() (handlers []any) {
})
})
+ // wrap the request and response, use the process context and add it to the process manager
handlers = append(handlers, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true)
diff --git a/routers/common/redirect.go b/routers/common/redirect.go
index 9bf2025e19..34044e814b 100644
--- a/routers/common/redirect.go
+++ b/routers/common/redirect.go
@@ -17,7 +17,7 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) {
// The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2",
// then frontend needs this delegate to redirect to the new location with hash correctly.
redirect := req.PostFormValue("redirect")
- if httplib.IsRiskyRedirectURL(redirect) {
+ if !httplib.IsCurrentGiteaSiteURL(redirect) {
resp.WriteHeader(http.StatusBadRequest)
return
}
diff --git a/routers/common/serve.go b/routers/common/serve.go
index 8a7f8b3332..446908db75 100644
--- a/routers/common/serve.go
+++ b/routers/common/serve.go
@@ -7,11 +7,11 @@ import (
"io"
"time"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
// ServeBlob download a git.Blob
diff --git a/routers/init.go b/routers/init.go
index e0a7150ba3..aaf95920c2 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -9,7 +9,6 @@ import (
"runtime"
"code.gitea.io/gitea/models"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
authmodel "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/eventsource"
@@ -33,6 +32,7 @@ import (
"code.gitea.io/gitea/routers/private"
web_routers "code.gitea.io/gitea/routers/web"
actions_service "code.gitea.io/gitea/services/actions"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/automerge"
@@ -94,7 +94,7 @@ func syncAppConfForGit(ctx context.Context) error {
mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
log.Info("re-write ssh public keys ...")
- mustInitCtx(ctx, asymkey_model.RewriteAllPublicKeys)
+ mustInitCtx(ctx, asymkey_service.RewriteAllPublicKeys)
return system.AppState.Set(ctx, runtimeState)
}
@@ -198,6 +198,8 @@ func NormalRoutes() *web.Route {
// TODO: this prefix should be generated with a token string with runner ?
prefix = "/api/actions_pipeline"
r.Mount(prefix, actions_router.ArtifactsRoutes(prefix))
+ prefix = actions_router.ArtifactV4RouteBase
+ r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix))
}
return r
diff --git a/routers/install/install.go b/routers/install/install.go
index 5c0290d2cc..9c6a8849b6 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -7,6 +7,7 @@ package install
import (
"fmt"
"net/http"
+ "net/mail"
"os"
"os/exec"
"path/filepath"
@@ -21,20 +22,20 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/user"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/common"
auth_service "code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"gitea.com/go-chi/session"
@@ -409,7 +410,7 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
var lfsJwtSecret string
- if _, lfsJwtSecret, err = generate.NewJwtSecretBase64(); err != nil {
+ if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil {
ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
return
}
@@ -419,6 +420,11 @@ func SubmitInstall(ctx *context.Context) {
}
if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
+ if _, err := mail.ParseAddress(form.SMTPFrom); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form)
+ return
+ }
+
cfg.Section("mailer").Key("ENABLED").SetValue("true")
cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr)
cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort)
@@ -533,8 +539,8 @@ func SubmitInstall(ctx *context.Context) {
IsAdmin: true,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsRestricted: util.OptionalBoolFalse,
- IsActive: util.OptionalBoolTrue,
+ IsRestricted: optional.Some(false),
+ IsActive: optional.Some(true),
}
if err = user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
diff --git a/routers/private/actions.go b/routers/private/actions.go
index 886f23b1c2..696634b5e7 100644
--- a/routers/private/actions.go
+++ b/routers/private/actions.go
@@ -12,11 +12,11 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
// GenerateActionsRunnerToken generates a new runner token for a given scope
@@ -26,7 +26,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
defer rd.Close()
if err := json.NewDecoder(rd).Decode(&genRequest); err != nil {
- log.Error("%v", err)
+ log.Error("JSON Decode failed: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
@@ -35,7 +35,7 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
owner, repo, err := parseScope(ctx, genRequest.Scope)
if err != nil {
- log.Error("%v", err)
+ log.Error("parseScope failed: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
@@ -45,18 +45,18 @@ func GenerateActionsRunnerToken(ctx *context.PrivateContext) {
if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
token, err = actions_model.NewRunnerToken(ctx, owner, repo)
if err != nil {
- err := fmt.Sprintf("error while creating runner token: %v", err)
- log.Error("%v", err)
+ errMsg := fmt.Sprintf("error while creating runner token: %v", err)
+ log.Error("NewRunnerToken failed: %v", errMsg)
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: err,
+ Err: errMsg,
})
return
}
} else if err != nil {
- err := fmt.Sprintf("could not get unactivated runner token: %v", err)
- log.Error("%v", err)
+ errMsg := fmt.Sprintf("could not get unactivated runner token: %v", err)
+ log.Error("GetLatestRunnerToken failed: %v", errMsg)
ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: err,
+ Err: errMsg,
})
return
}
diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go
index a23e101e9d..33890be6a9 100644
--- a/routers/private/default_branch.go
+++ b/routers/private/default_branch.go
@@ -8,9 +8,10 @@ import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/private"
+ gitea_context "code.gitea.io/gitea/services/context"
)
// SetDefaultBranch updates the default branch
@@ -20,7 +21,7 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
branch := ctx.Params(":branch")
ctx.Repo.Repository.DefaultBranch = branch
- if err := ctx.Repo.GitRepo.SetDefaultBranch(ctx.Repo.Repository.DefaultBranch); err != nil {
+ if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err),
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 1b274ae154..769a68970d 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -6,18 +6,22 @@ package private
import (
"fmt"
"net/http"
- "strconv"
+ git_model "code.gitea.io/gitea/models/git"
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"
- gitea_context "code.gitea.io/gitea/modules/context"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ gitea_context "code.gitea.io/gitea/services/context"
+ pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -27,6 +31,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
// We don't rely on RepoAssignment here because:
// a) we don't need the git repo in this function
+ // OUT OF DATE: we do need the git repo to sync the branch to the db now.
// b) our update function will likely change the repository in the db so we will need to refresh it
// c) we don't always need the repo
@@ -34,7 +39,11 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
repoName := ctx.Params(":repo")
// defer getting the repository at this point - as we should only retrieve it if we're going to call update
- var repo *repo_model.Repository
+ var (
+ repo *repo_model.Repository
+ gitRepo *git.Repository
+ )
+ defer gitRepo.Close() // it's safe to call Close on a nil pointer
updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs))
wasEmpty := false
@@ -68,6 +77,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
updates = append(updates, option)
if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
// put the master/main branch first
+ // FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
+ // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
+ // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
+ // If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
copy(updates[1:], updates)
updates[0] = option
}
@@ -75,6 +88,64 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
}
if repo != nil && len(updates) > 0 {
+ branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates))
+ for _, update := range updates {
+ if !update.RefFullName.IsBranch() {
+ continue
+ }
+ if repo == nil {
+ repo = loadRepository(ctx, ownerName, repoName)
+ if ctx.Written() {
+ return
+ }
+ wasEmpty = repo.IsEmpty
+ }
+
+ if update.IsDelRef() {
+ if err := git_model.AddDeletedBranch(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil {
+ log.Error("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err)
+ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+ Err: fmt.Sprintf("Failed to add deleted branch: %s/%s Error: %v", ownerName, repoName, err),
+ })
+ return
+ }
+ } else {
+ branchesToSync = append(branchesToSync, update)
+
+ // TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing
+ pull_service.UpdatePullsRefs(ctx, repo, update)
+ }
+ }
+ if len(branchesToSync) > 0 {
+ if gitRepo == nil {
+ var err error
+ gitRepo, err = gitrepo.OpenRepository(ctx, repo)
+ if err != nil {
+ log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
+ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+ Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
+ })
+ return
+ }
+ }
+
+ var (
+ branchNames = make([]string, 0, len(branchesToSync))
+ commitIDs = make([]string, 0, len(branchesToSync))
+ )
+ for _, update := range branchesToSync {
+ branchNames = append(branchNames, update.RefFullName.BranchName())
+ commitIDs = append(commitIDs, update.NewCommitID)
+ }
+
+ if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil {
+ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+ Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err),
+ })
+ return
+ }
+ }
+
if err := repo_service.PushUpdates(updates); err != nil {
log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates))
for i, update := range updates {
@@ -89,8 +160,10 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
}
}
+ isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate)
+ isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate)
// Handle Push Options
- if len(opts.GitPushOptions) > 0 {
+ if isPrivate.Has() || isTemplate.Has() {
// load the repository
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
@@ -101,13 +174,49 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
wasEmpty = repo.IsEmpty
}
- repo.IsPrivate = opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate, repo.IsPrivate)
- repo.IsTemplate = opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate, repo.IsTemplate)
- if err := repo_model.UpdateRepositoryCols(ctx, repo, "is_private", "is_template"); err != nil {
+ pusher, err := user_model.GetUserByID(ctx, opts.UserID)
+ if err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
})
+ return
+ }
+ perm, err := access_model.GetUserRepoPermission(ctx, repo, pusher)
+ if err != nil {
+ log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+ Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+ })
+ return
+ }
+ if !perm.IsOwner() && !perm.IsAdmin() {
+ ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{
+ Err: "Permissions denied",
+ })
+ return
+ }
+
+ cols := make([]string, 0, len(opts.GitPushOptions))
+
+ if isPrivate.Has() {
+ repo.IsPrivate = isPrivate.Value()
+ cols = append(cols, "is_private")
+ }
+
+ if isTemplate.Has() {
+ repo.IsTemplate = isTemplate.Value()
+ cols = append(cols, "is_template")
+ }
+
+ if len(cols) > 0 {
+ if err := repo_model.UpdateRepositoryCols(ctx, repo, cols...); err != nil {
+ log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
+ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
+ Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
+ })
+ return
+ }
}
}
@@ -122,44 +231,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
refFullName := opts.RefFullNames[i]
newCommitID := opts.NewCommitIDs[i]
- // post update for agit pull request
- // FIXME: use pr.Flow to test whether it's an Agit PR or a GH PR
- if git.SupportProcReceive && refFullName.IsPull() {
- if repo == nil {
- repo = loadRepository(ctx, ownerName, repoName)
- if ctx.Written() {
- return
- }
- }
-
- pullIndex, _ := strconv.ParseInt(refFullName.PullName(), 10, 64)
- if pullIndex <= 0 {
- continue
- }
-
- pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex)
- if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
- log.Error("Failed to get PR by index %v Error: %v", pullIndex, err)
- ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err),
- })
- return
- }
- if pr == nil {
- continue
- }
-
- results = append(results, private.HookPostReceiveBranchResult{
- Message: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
- Create: false,
- Branch: "",
- URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
- })
- continue
- }
-
// If we've pushed a branch (and not deleted it)
- if git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
+ if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() {
// First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo
if repo == nil {
repo = loadRepository(ctx, ownerName, repoName)
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index 90d8287f06..32ec3003e2 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -16,11 +16,11 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/web"
+ gitea_context "code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull"
)
@@ -122,7 +122,7 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
preReceiveBranch(ourCtx, oldCommitID, newCommitID, refFullName)
case refFullName.IsTag():
preReceiveTag(ourCtx, oldCommitID, newCommitID, refFullName)
- case git.SupportProcReceive && refFullName.IsFor():
+ case git.DefaultFeatures.SupportProcReceive && refFullName.IsFor():
preReceiveFor(ourCtx, oldCommitID, newCommitID, refFullName)
default:
ourCtx.AssertCanWriteCode()
@@ -145,7 +145,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
repo := ctx.Repo.Repository
gitRepo := ctx.Repo.GitRepo
- objectFormat, _ := gitRepo.GetObjectFormat()
+ objectFormat := ctx.Repo.GetObjectFormat()
if branchName == repo.DefaultBranch && newCommitID == objectFormat.EmptyObjectID().String() {
log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go
index 5577120770..cee3bbdd12 100644
--- a/routers/private/hook_proc_receive.go
+++ b/routers/private/hook_proc_receive.go
@@ -7,18 +7,18 @@ import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/agit"
+ gitea_context "code.gitea.io/gitea/services/context"
)
// HookProcReceive proc-receive hook - only handles agit Proc-Receive requests at present
func HookProcReceive(ctx *gitea_context.PrivateContext) {
opts := web.GetForm(ctx).(*private.HookOptions)
- if !git.SupportProcReceive {
+ if !git.DefaultFeatures.SupportProcReceive {
ctx.Status(http.StatusNotFound)
return
}
diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go
index 42b8e5abed..764c976fa9 100644
--- a/routers/private/hook_verification.go
+++ b/routers/private/hook_verification.go
@@ -47,7 +47,7 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []
_ = stdoutWriter.Close()
err := readAndVerifyCommitsFromShaReader(stdoutReader, repo, env)
if err != nil {
- log.Error("%v", err)
+ log.Error("readAndVerifyCommitsFromShaReader failed: %v", err)
cancel()
}
_ = stdoutReader.Close()
@@ -66,7 +66,6 @@ func readAndVerifyCommitsFromShaReader(input io.ReadCloser, repo *git.Repository
line := scanner.Text()
err := readAndVerifyCommit(line, repo, env)
if err != nil {
- log.Error("%v", err)
return err
}
}
diff --git a/routers/private/internal.go b/routers/private/internal.go
index 407edebeed..ede310113c 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -8,11 +8,11 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
chi_middleware "github.com/go-chi/chi/v5/middleware"
diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go
index 615239d479..e8ee8ba8ac 100644
--- a/routers/private/internal_repo.go
+++ b/routers/private/internal_repo.go
@@ -9,10 +9,10 @@ import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
+ gitea_context "code.gitea.io/gitea/services/context"
)
// This file contains common functions relating to setting the Repository for the internal routes
diff --git a/routers/private/key.go b/routers/private/key.go
index 0096480d6a..5b8f238a83 100644
--- a/routers/private/key.go
+++ b/routers/private/key.go
@@ -7,9 +7,9 @@ import (
"net/http"
asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/services/context"
)
// UpdatePublicKeyInRepo update public key and deploy key updates
diff --git a/routers/private/mail.go b/routers/private/mail.go
index e5e162c880..cf3abb31c6 100644
--- a/routers/private/mail.go
+++ b/routers/private/mail.go
@@ -11,11 +11,11 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/mailer"
)
@@ -35,7 +35,7 @@ func SendEmail(ctx *context.PrivateContext) {
defer rd.Close()
if err := json.NewDecoder(rd).Decode(&mail); err != nil {
- log.Error("%v", err)
+ log.Error("JSON Decode failed: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
})
diff --git a/routers/private/manager.go b/routers/private/manager.go
index 397e6fac7b..a6aa03e4ec 100644
--- a/routers/private/manager.go
+++ b/routers/private/manager.go
@@ -8,7 +8,6 @@ import (
"net/http"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/graceful/releasereopen"
"code.gitea.io/gitea/modules/log"
@@ -17,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
)
// ReloadTemplates reloads all the templates
diff --git a/routers/private/manager_process.go b/routers/private/manager_process.go
index 68e4a21805..9a0298a37c 100644
--- a/routers/private/manager_process.go
+++ b/routers/private/manager_process.go
@@ -11,10 +11,10 @@ import (
"runtime"
"time"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
process_module "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/services/context"
)
// Processes prints out the processes
diff --git a/routers/private/manager_unix.go b/routers/private/manager_unix.go
index 09ced33b8d..0c63ebc918 100644
--- a/routers/private/manager_unix.go
+++ b/routers/private/manager_unix.go
@@ -8,8 +8,8 @@ package private
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/services/context"
)
// Restart causes the server to perform a graceful restart
diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go
index bd3c3c30d0..f1b9365f52 100644
--- a/routers/private/manager_windows.go
+++ b/routers/private/manager_windows.go
@@ -8,9 +8,9 @@ package private
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/private"
+ "code.gitea.io/gitea/services/context"
)
// Restart is not implemented for Windows based servers as they can't fork
diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go
index 7efc22a3d9..4e95d3071d 100644
--- a/routers/private/restore_repo.go
+++ b/routers/private/restore_repo.go
@@ -7,9 +7,9 @@ import (
"io"
"net/http"
- myCtx "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/private"
+ myCtx "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/migrations"
)
diff --git a/routers/private/serv.go b/routers/private/serv.go
index 00731947a5..85368a0aed 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -14,11 +14,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
wiki_service "code.gitea.io/gitea/services/wiki"
)
@@ -297,7 +297,7 @@ func ServCommand(ctx *context.PrivateContext) {
}
} else {
// Because of the special ref "refs/for" we will need to delay write permission check
- if git.SupportProcReceive && unitType == unit.TypeCode {
+ if git.DefaultFeatures.SupportProcReceive && unitType == unit.TypeCode {
mode = perm.AccessModeRead
}
diff --git a/routers/private/ssh_log.go b/routers/private/ssh_log.go
index eacfa18f05..5bec632ead 100644
--- a/routers/private/ssh_log.go
+++ b/routers/private/ssh_log.go
@@ -6,11 +6,11 @@ package private
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
)
// SSHLog hook to response ssh log
diff --git a/routers/utils/utils.go b/routers/utils/utils.go
index 1f4d11fd3c..3035073d5c 100644
--- a/routers/utils/utils.go
+++ b/routers/utils/utils.go
@@ -5,26 +5,10 @@ package utils
import (
"html"
- "net/url"
"strings"
-
- "code.gitea.io/gitea/modules/setting"
)
// SanitizeFlashErrorString will sanitize a flash error string
func SanitizeFlashErrorString(x string) string {
return strings.ReplaceAll(html.EscapeString(x), "\n", "
")
}
-
-// IsExternalURL checks if rawURL points to an external URL like http://example.com
-func IsExternalURL(rawURL string) bool {
- parsed, err := url.Parse(rawURL)
- if err != nil {
- return true
- }
- appURL, _ := url.Parse(setting.AppURL)
- if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(appURL.Host, "www.", "", 1) {
- return true
- }
- return false
-}
diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go
index 440aad87c6..6e7f3c33cd 100644
--- a/routers/utils/utils_test.go
+++ b/routers/utils/utils_test.go
@@ -5,47 +5,8 @@ package utils
import (
"testing"
-
- "code.gitea.io/gitea/modules/setting"
-
- "github.com/stretchr/testify/assert"
)
-func TestIsExternalURL(t *testing.T) {
- setting.AppURL = "https://try.gitea.io/"
- type test struct {
- Expected bool
- RawURL string
- }
- newTest := func(expected bool, rawURL string) test {
- return test{Expected: expected, RawURL: rawURL}
- }
- for _, test := range []test{
- newTest(false,
- "https://try.gitea.io"),
- newTest(true,
- "https://example.com/"),
- newTest(true,
- "//example.com"),
- newTest(true,
- "http://example.com"),
- newTest(false,
- "a/"),
- newTest(false,
- "https://try.gitea.io/test?param=false"),
- newTest(false,
- "test?param=false"),
- newTest(false,
- "//try.gitea.io/test?param=false"),
- newTest(false,
- "/hey/hey/hey#3244"),
- newTest(true,
- "://missing protocol scheme"),
- } {
- assert.Equal(t, test.Expected, IsExternalURL(test.RawURL))
- }
-}
-
func TestSanitizeFlashErrorString(t *testing.T) {
tests := []struct {
name string
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index d31cb1cd25..e6585d8833 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -14,13 +14,13 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/updatechecker"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/cron"
"code.gitea.io/gitea/services/forms"
release_service "code.gitea.io/gitea/services/release"
@@ -28,13 +28,14 @@ import (
)
const (
- tplDashboard base.TplName = "admin/dashboard"
- tplSelfCheck base.TplName = "admin/self_check"
- tplCron base.TplName = "admin/cron"
- tplQueue base.TplName = "admin/queue"
- tplStacktrace base.TplName = "admin/stacktrace"
- tplQueueManage base.TplName = "admin/queue_manage"
- tplStats base.TplName = "admin/stats"
+ tplDashboard base.TplName = "admin/dashboard"
+ tplSystemStatus base.TplName = "admin/system_status"
+ tplSelfCheck base.TplName = "admin/self_check"
+ tplCron base.TplName = "admin/cron"
+ tplQueue base.TplName = "admin/queue"
+ tplStacktrace base.TplName = "admin/stacktrace"
+ tplQueueManage base.TplName = "admin/queue_manage"
+ tplStats base.TplName = "admin/stats"
)
var sysStatus struct {
@@ -72,7 +73,7 @@ var sysStatus struct {
// Garbage collector statistics.
NextGC string // next run in HeapAlloc time (bytes)
- LastGC string // last run in absolute time (ns)
+ LastGCTime string // last run time
PauseTotalNs string
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
NumGC uint32
@@ -110,17 +111,17 @@ func updateSystemStatus() {
sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
sysStatus.NextGC = base.FileSize(int64(m.NextGC))
- sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
+ sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339)
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
sysStatus.NumGC = m.NumGC
}
-func prepareDeprecatedWarningsAlert(ctx *context.Context) {
- if len(setting.DeprecatedWarnings) > 0 {
- content := setting.DeprecatedWarnings[0]
- if len(setting.DeprecatedWarnings) > 1 {
- content += fmt.Sprintf(" (and %d more)", len(setting.DeprecatedWarnings)-1)
+func prepareStartupProblemsAlert(ctx *context.Context) {
+ if len(setting.StartupProblems) > 0 {
+ content := setting.StartupProblems[0]
+ if len(setting.StartupProblems) > 1 {
+ content += fmt.Sprintf(" (and %d more)", len(setting.StartupProblems)-1)
}
ctx.Flash.Error(content, true)
}
@@ -132,14 +133,19 @@ func Dashboard(ctx *context.Context) {
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx)
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
- // FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.Data["SSH"] = setting.SSH
- prepareDeprecatedWarningsAlert(ctx)
+ prepareStartupProblemsAlert(ctx)
ctx.HTML(http.StatusOK, tplDashboard)
}
+func SystemStatus(ctx *context.Context) {
+ updateSystemStatus()
+ ctx.Data["SysStatus"] = sysStatus
+ ctx.HTML(http.StatusOK, tplSystemStatus)
+}
+
// DashboardPost run an admin operation
func DashboardPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminDashboardForm)
@@ -184,6 +190,14 @@ func DashboardPost(ctx *context.Context) {
func SelfCheck(ctx *context.Context) {
ctx.Data["PageIsAdminSelfCheck"] = true
+
+ ctx.Data["StartupProblems"] = setting.StartupProblems
+ if len(setting.StartupProblems) == 0 && !setting.IsProd {
+ if time.Now().Unix()%2 == 0 {
+ ctx.Data["StartupProblems"] = []string{"This is a test warning message in dev mode"}
+ }
+ }
+
r, err := db.CheckCollationsDefaultEngine()
if err != nil {
ctx.Flash.Error(fmt.Sprintf("CheckCollationsDefaultEngine: %v", err), true)
diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go
index b6f7bcd2a5..8583398074 100644
--- a/routers/web/admin/applications.go
+++ b/routers/web/admin/applications.go
@@ -10,9 +10,9 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
)
var (
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 2cf63c646d..ba487d1045 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -16,7 +16,6 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/auth/pam"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -27,6 +26,7 @@ import (
pam_service "code.gitea.io/gitea/services/auth/source/pam"
"code.gitea.io/gitea/services/auth/source/smtp"
"code.gitea.io/gitea/services/auth/source/sspi"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"xorm.io/xorm/convert"
@@ -210,16 +210,16 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
- return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error"))
+ return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
}
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true
- return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error"))
+ return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
}
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
ctx.Data["Err_SSPIDefaultLanguage"] = true
- return nil, errors.New(ctx.Tr("form.lang_select_error"))
+ return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
}
return &sspi.Source{
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index c827f2a4f5..48f80dbbf1 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -7,24 +7,27 @@ package admin
import (
"net/http"
"net/url"
+ "strconv"
"strings"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/mailer"
"gitea.com/go-chi/session"
)
-const tplConfig base.TplName = "admin/config"
+const (
+ tplConfig base.TplName = "admin/config"
+ tplConfigSettings base.TplName = "admin/config_settings"
+)
// SendTestMail send test mail to confirm mail service is OK
func SendTestMail(ctx *context.Context) {
@@ -98,8 +101,9 @@ func shadowPassword(provider, cfgItem string) string {
// Config show admin config page
func Config(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("admin.config")
+ ctx.Data["Title"] = ctx.Tr("admin.config_summary")
ctx.Data["PageIsAdminConfig"] = true
+ ctx.Data["PageIsAdminConfigSummary"] = true
ctx.Data["CustomConf"] = setting.CustomConf
ctx.Data["AppUrl"] = setting.AppURL
@@ -161,23 +165,70 @@ func Config(ctx *context.Context) {
ctx.Data["Loggers"] = log.GetManager().DumpLoggers()
config.GetDynGetter().InvalidateCache()
- ctx.Data["SystemConfig"] = setting.Config()
- prepareDeprecatedWarningsAlert(ctx)
+ prepareStartupProblemsAlert(ctx)
ctx.HTML(http.StatusOK, tplConfig)
}
+func ConfigSettings(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("admin.config_settings")
+ ctx.Data["PageIsAdminConfig"] = true
+ ctx.Data["PageIsAdminConfigSettings"] = true
+ ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
+ ctx.HTML(http.StatusOK, tplConfigSettings)
+}
+
func ChangeConfig(ctx *context.Context) {
key := strings.TrimSpace(ctx.FormString("key"))
value := ctx.FormString("value")
cfg := setting.Config()
- allowedKeys := container.SetOf(cfg.Picture.DisableGravatar.DynKey(), cfg.Picture.EnableFederatedAvatar.DynKey())
- if !allowedKeys.Contains(key) {
+
+ marshalBool := func(v string) (string, error) {
+ if b, _ := strconv.ParseBool(v); b {
+ return "true", nil
+ }
+ return "false", nil
+ }
+ marshalOpenWithApps := func(value string) (string, error) {
+ lines := strings.Split(value, "\n")
+ var openWithEditorApps setting.OpenWithEditorAppsType
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ displayName, openURL, ok := strings.Cut(line, "=")
+ displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
+ if !ok || displayName == "" || openURL == "" {
+ continue
+ }
+ openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
+ DisplayName: strings.TrimSpace(displayName),
+ OpenURL: strings.TrimSpace(openURL),
+ })
+ }
+ b, err := json.Marshal(openWithEditorApps)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+ }
+ marshallers := map[string]func(string) (string, error){
+ cfg.Picture.DisableGravatar.DynKey(): marshalBool,
+ cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
+ cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
+ }
+ marshaller, hasMarshaller := marshallers[key]
+ if !hasMarshaller {
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
return
}
- if err := system_model.SetSettings(ctx, map[string]string{key: value}); err != nil {
- log.Error("set setting failed: %v", err)
+ marshaledValue, err := marshaller(value)
+ if err != nil {
+ ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
+ return
+ }
+ if err = system_model.SetSettings(ctx, map[string]string{key: marshaledValue}); err != nil {
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
return
}
diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go
index 2d550125d5..020554a35a 100644
--- a/routers/web/admin/diagnosis.go
+++ b/routers/web/admin/diagnosis.go
@@ -9,8 +9,8 @@ import (
"runtime/pprof"
"time"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httplib"
+ "code.gitea.io/gitea/services/context"
)
func MonitorDiagnosis(ctx *context.Context) {
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
index 59f80035d8..2cf4035c6a 100644
--- a/routers/web/admin/emails.go
+++ b/routers/web/admin/emails.go
@@ -11,10 +11,10 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -68,10 +68,10 @@ func Emails(ctx *context.Context) {
opts.Keyword = ctx.FormTrim("q")
opts.SortType = orderBy
if len(ctx.FormString("is_activated")) != 0 {
- opts.IsActivated = util.OptionalBoolOf(ctx.FormBool("activated"))
+ opts.IsActivated = optional.Some(ctx.FormBool("activated"))
}
if len(ctx.FormString("is_primary")) != 0 {
- opts.IsPrimary = util.OptionalBoolOf(ctx.FormBool("primary"))
+ opts.IsPrimary = optional.Some(ctx.FormBool("primary"))
}
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index cd8cc29cdf..8d59fbb858 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -8,9 +8,9 @@ import (
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -35,7 +35,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
sys["Title"] = ctx.Tr("admin.systemhooks")
sys["Description"] = ctx.Tr("admin.systemhooks.desc")
- sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
+ sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
if err != nil {
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
index e1cb578d05..36303cbc06 100644
--- a/routers/web/admin/notice.go
+++ b/routers/web/admin/notice.go
@@ -11,9 +11,9 @@ import (
"code.gitea.io/gitea/models/db"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 00131c9e2f..c5454db71e 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -8,10 +8,10 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/web/explore"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index 35ce215be4..39f064a1be 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -11,9 +11,9 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
)
@@ -36,7 +36,7 @@ func Packages(ctx *context.Context) {
Type: packages_model.Type(packageType),
Name: packages_model.SearchValue{Value: query},
Sort: sort,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Paginator: &db.ListOptions{
PageSize: setting.UI.PackagesPagingNum,
Page: page,
diff --git a/routers/web/admin/queue.go b/routers/web/admin/queue.go
index 18a8d7d3e6..d8c50730b1 100644
--- a/routers/web/admin/queue.go
+++ b/routers/web/admin/queue.go
@@ -7,9 +7,9 @@ import (
"net/http"
"strconv"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
func Queues(ctx *context.Context) {
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index 45c280ef73..0815879bb3 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -4,6 +4,7 @@
package admin
import (
+ "fmt"
"net/http"
"net/url"
"strings"
@@ -12,11 +13,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/explore"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -84,7 +85,7 @@ func UnadoptedRepos(ctx *context.Context) {
if !doSearch {
pager := context.NewPagination(0, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "search", "search")
+ pager.AddParamString("search", fmt.Sprint(doSearch))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
return
@@ -98,7 +99,7 @@ func UnadoptedRepos(ctx *context.Context) {
ctx.Data["Dirs"] = repoNames
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "search", "search")
+ pager.AddParamString("search", fmt.Sprint(doSearch))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUnadoptedRepos)
}
diff --git a/routers/web/admin/runners.go b/routers/web/admin/runners.go
index eaa268b4f1..d73290a8db 100644
--- a/routers/web/admin/runners.go
+++ b/routers/web/admin/runners.go
@@ -4,8 +4,8 @@
package admin
import (
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go
index b603fb59a2..d6def94bb4 100644
--- a/routers/web/admin/stacktrace.go
+++ b/routers/web/admin/stacktrace.go
@@ -7,9 +7,9 @@ import (
"net/http"
"runtime"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
// Stacktrace show admin monitor goroutines page
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index af184fa9eb..ea9d6f4c9c 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -19,7 +19,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@@ -27,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/explore"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
@@ -96,7 +96,7 @@ func NewUser(ctx *context.Context) {
ctx.Data["login_type"] = "0-0"
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
})
if err != nil {
ctx.ServerError("auth.Sources", err)
@@ -117,7 +117,7 @@ func NewUserPost(ctx *context.Context) {
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
})
if err != nil {
ctx.ServerError("auth.Sources", err)
@@ -140,7 +140,7 @@ func NewUserPost(ctx *context.Context) {
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
Visibility: &form.Visibility,
}
@@ -177,7 +177,7 @@ func NewUserPost(ctx *context.Context) {
u.MustChangePassword = form.MustChangePassword
}
- if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
+ if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
@@ -202,6 +202,11 @@ func NewUserPost(ctx *context.Context) {
}
return
}
+
+ if !user_model.IsEmailDomainAllowed(u.Email) {
+ ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
+ }
+
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
@@ -270,13 +275,11 @@ func ViewUser(ctx *context.Context) {
}
repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
- ListOptions: db.ListOptions{
- ListAll: true,
- },
+ ListOptions: db.ListOptionsAll,
OwnerID: u.ID,
OrderBy: db.SearchOrderByAlphabetically,
Private: true,
- Collaborate: util.OptionalBoolFalse,
+ Collaborate: optional.Some(false),
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -295,9 +298,7 @@ func ViewUser(ctx *context.Context) {
ctx.Data["EmailsTotal"] = len(emails)
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
- ListOptions: db.ListOptions{
- ListAll: true,
- },
+ ListOptions: db.ListOptionsAll,
UserID: u.ID,
IncludePrivate: true,
})
@@ -402,7 +403,6 @@ func EditUserPost(ctx *context.Context) {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplUserEdit, &form)
case password.IsErrIsPwnedRequest(err):
- log.Error("%s", err.Error())
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
default:
@@ -412,7 +412,7 @@ func EditUserPost(ctx *context.Context) {
}
if form.Email != "" {
- if err := user_service.AddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
+ if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true
@@ -425,6 +425,9 @@ func EditUserPost(ctx *context.Context) {
}
return
}
+ if !user_model.IsEmailDomainAllowed(form.Email) {
+ ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
+ }
}
opts := &user_service.UpdateOptions{
@@ -439,6 +442,7 @@ func EditUserPost(ctx *context.Context) {
AllowCreateOrganization: optional.Some(form.AllowCreateOrganization),
IsRestricted: optional.Some(form.Restricted),
Visibility: optional.Some(form.Visibility),
+ Language: optional.Some(form.Language),
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
index 560ee70ea0..f6f9237858 100644
--- a/routers/web/admin/users_test.go
+++ b/routers/web/admin/users_test.go
@@ -8,10 +8,10 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go
index dc0062ebaa..f93177bf96 100644
--- a/routers/web/auth/2fa.go
+++ b/routers/web/auth/2fa.go
@@ -10,9 +10,9 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 3de1f3373d..8b5cd986b8 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -7,6 +7,7 @@ package auth
import (
"errors"
"fmt"
+ "html/template"
"net/http"
"strings"
@@ -15,8 +16,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
@@ -25,9 +26,9 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/routers/utils"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
@@ -37,12 +38,10 @@ import (
)
const (
- // tplSignIn template for sign in page
- tplSignIn base.TplName = "user/auth/signin"
- // tplSignUp template path for sign up page
- tplSignUp base.TplName = "user/auth/signup"
- // TplActivate template path for activate user
- TplActivate base.TplName = "user/auth/activate"
+ tplSignIn base.TplName = "user/auth/signin" // for sign in page
+ tplSignUp base.TplName = "user/auth/signup" // for sign up page
+ TplActivate base.TplName = "user/auth/activate" // for activate user
+ TplActivatePrompt base.TplName = "user/auth/activate_prompt" // for showing a message for user activation
)
// autoSignIn reads cookie and try to auto-login.
@@ -124,9 +123,21 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
return nil
}
+func RedirectAfterLogin(ctx *context.Context) {
+ redirectTo := ctx.FormString("redirect_to")
+ if redirectTo == "" {
+ redirectTo = ctx.GetSiteCookie("redirect_to")
+ }
+ middleware.DeleteRedirectToCookie(ctx.Resp)
+ nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
+ if setting.LandingPageURL == setting.LandingPageLogin {
+ nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
+ }
+ ctx.RedirectToCurrentSite(redirectTo, nextRedirectTo)
+}
+
func CheckAutoLogin(ctx *context.Context) bool {
- // Check auto-login
- isSucceed, err := autoSignIn(ctx)
+ isSucceed, err := autoSignIn(ctx) // try to auto-login
if err != nil {
if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) {
ctx.Flash.Error(ctx.Tr("auth.remember_me.compromised"), true)
@@ -139,17 +150,10 @@ func CheckAutoLogin(ctx *context.Context) bool {
redirectTo := ctx.FormString("redirect_to")
if len(redirectTo) > 0 {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
- } else {
- redirectTo = ctx.GetSiteCookie("redirect_to")
}
if isSucceed {
- middleware.DeleteRedirectToCookie(ctx.Resp)
- nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
- if setting.LandingPageURL == setting.LandingPageLogin {
- nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
- }
- ctx.RedirectToFirst(redirectTo, nextRedirectTo)
+ RedirectAfterLogin(ctx)
return true
}
@@ -164,7 +168,12 @@ func SignIn(ctx *context.Context) {
return
}
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ if ctx.IsSigned {
+ RedirectAfterLogin(ctx)
+ return
+ }
+
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -187,7 +196,7 @@ func SignIn(ctx *context.Context) {
func SignInPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -359,10 +368,10 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/"
}
- if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+ if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp)
if obeyRedirect {
- ctx.RedirectToFirst(redirectTo)
+ ctx.RedirectToCurrentSite(redirectTo)
}
return redirectTo
}
@@ -411,7 +420,7 @@ func SignUp(ctx *context.Context) {
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
return
@@ -440,7 +449,7 @@ func SignUpPost(ctx *context.Context) {
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
- oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, util.OptionalBoolTrue)
+ oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
return
@@ -613,72 +622,87 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
}
}
- // Send confirmation email
- if !u.IsActive && u.ID > 1 {
- if setting.Service.RegisterManualConfirm {
- ctx.Data["ManualActivationOnly"] = true
- ctx.HTML(http.StatusOK, TplActivate)
- return false
- }
+ // for active user or the first (admin) user, we don't need to send confirmation email
+ if u.IsActive || u.ID == 1 {
+ return true
+ }
- mailer.SendActivateAccountMail(ctx.Locale, u)
-
- ctx.Data["IsSendRegisterMail"] = true
- ctx.Data["Email"] = u.Email
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
- ctx.HTML(http.StatusOK, TplActivate)
-
- if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
+ if setting.Service.RegisterManualConfirm {
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.manual_activation_only"))
return false
}
- return true
+ sendActivateEmail(ctx, u)
+ return false
+}
+
+func renderActivationPromptMessage(ctx *context.Context, msg template.HTML) {
+ ctx.Data["ActivationPromptMessage"] = msg
+ ctx.HTML(http.StatusOK, TplActivatePrompt)
+}
+
+func sendActivateEmail(ctx *context.Context, u *user_model.User) {
+ if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+ return
+ }
+
+ if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.resent_limit_prompt"))
+ return
+ }
+
+ mailer.SendActivateAccountMail(ctx.Locale, u)
+
+ activeCodeLives := timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
+ msgHTML := ctx.Locale.Tr("auth.confirmation_mail_sent_prompt_ex", u.Email, activeCodeLives)
+ renderActivationPromptMessage(ctx, msgHTML)
+}
+
+func renderActivationVerifyPassword(ctx *context.Context, code string) {
+ ctx.Data["ActivationCode"] = code
+ ctx.Data["NeedVerifyLocalPassword"] = true
+ ctx.HTML(http.StatusOK, TplActivate)
+}
+
+func renderActivationChangeEmail(ctx *context.Context) {
+ ctx.HTML(http.StatusOK, TplActivate)
}
// Activate render activate user page
func Activate(ctx *context.Context) {
code := ctx.FormString("code")
- if len(code) == 0 {
- ctx.Data["IsActivatePage"] = true
- if ctx.Doer == nil || ctx.Doer.IsActive {
- ctx.NotFound("invalid user", nil)
+ if code == "" {
+ if ctx.Doer == nil {
+ ctx.Redirect(setting.AppSubURL + "/user/login")
+ return
+ } else if ctx.Doer.IsActive {
+ ctx.Redirect(setting.AppSubURL + "/")
return
}
- // Resend confirmation email.
- if setting.Service.RegisterEmailConfirm {
- if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) {
- ctx.Data["ResendLimited"] = true
- } else {
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
- mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
- if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
- }
- }
- } else {
- ctx.Data["ServiceNotEnabled"] = true
+ if setting.MailService == nil || !setting.Service.RegisterEmailConfirm {
+ renderActivationPromptMessage(ctx, ctx.Tr("auth.disable_register_mail"))
+ return
}
- ctx.HTML(http.StatusOK, TplActivate)
+
+ // Resend confirmation email. FIXME: ideally this should be in a POST request
+ sendActivateEmail(ctx, ctx.Doer)
return
}
+ // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
user := user_model.VerifyUserActiveCode(ctx, code)
- // if code is wrong
- if user == nil {
- ctx.Data["IsCodeInvalid"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ if user == nil { // if code is wrong
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
return
}
// if account is local account, verify password
if user.LoginSource == 0 {
- ctx.Data["Code"] = code
- ctx.Data["NeedsPassword"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ renderActivationVerifyPassword(ctx, code)
return
}
@@ -688,31 +712,49 @@ func Activate(ctx *context.Context) {
// ActivatePost handles account activation with password check
func ActivatePost(ctx *context.Context) {
code := ctx.FormString("code")
- if len(code) == 0 {
+ if ctx.Doer != nil && ctx.Doer.IsActive {
+ ctx.Redirect(setting.AppSubURL + "/user/activate") // it will redirect again to the correct page
+ return
+ }
+
+ if code == "" {
+ newEmail := strings.TrimSpace(ctx.FormString("change_email"))
+ if ctx.Doer != nil && newEmail != "" && !strings.EqualFold(ctx.Doer.Email, newEmail) {
+ if user_model.ValidateEmail(newEmail) != nil {
+ ctx.Flash.Error(ctx.Locale.Tr("form.email_invalid"), true)
+ renderActivationChangeEmail(ctx)
+ return
+ }
+ err := user_model.ChangeInactivePrimaryEmail(ctx, ctx.Doer.ID, ctx.Doer.Email, newEmail)
+ if err != nil {
+ ctx.Flash.Error(ctx.Locale.Tr("admin.emails.not_updated", newEmail), true)
+ renderActivationChangeEmail(ctx)
+ return
+ }
+ ctx.Doer.Email = newEmail
+ }
+ // FIXME: at the moment, GET request handles the "send confirmation email" action. But the old code does this redirect and then send a confirmation email.
ctx.Redirect(setting.AppSubURL + "/user/activate")
return
}
+ // TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
user := user_model.VerifyUserActiveCode(ctx, code)
- // if code is wrong
- if user == nil {
- ctx.Data["IsCodeInvalid"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ if user == nil { // if code is wrong
+ renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
return
}
// if account is local account, verify password
if user.LoginSource == 0 {
password := ctx.FormString("password")
- if len(password) == 0 {
- ctx.Data["Code"] = code
- ctx.Data["NeedsPassword"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ if password == "" {
+ renderActivationVerifyPassword(ctx, code)
return
}
if !user.ValidatePassword(password) {
- ctx.Data["IsPasswordInvalid"] = true
- ctx.HTML(http.StatusOK, TplActivate)
+ ctx.Flash.Error(ctx.Locale.Tr("auth.invalid_password"), true)
+ renderActivationVerifyPassword(ctx, code)
return
}
}
@@ -766,7 +808,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
ctx.Flash.Success(ctx.Tr("auth.account_activated"))
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
middleware.DeleteRedirectToCookie(ctx.Resp)
- ctx.RedirectToFirst(redirectTo)
+ ctx.RedirectToCurrentSite(redirectTo)
return
}
diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go
new file mode 100644
index 0000000000..c6afbf877c
--- /dev/null
+++ b/routers/web/auth/auth_test.go
@@ -0,0 +1,43 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUserLogin(t *testing.T) {
+ ctx, resp := contexttest.MockContext(t, "/user/login")
+ SignIn(ctx)
+ assert.Equal(t, http.StatusOK, resp.Code)
+
+ ctx, resp = contexttest.MockContext(t, "/user/login")
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, http.StatusSeeOther, resp.Code)
+ assert.Equal(t, "/", test.RedirectURL(resp))
+
+ ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to=/other")
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, "/other", test.RedirectURL(resp))
+
+ ctx, resp = contexttest.MockContext(t, "/user/login")
+ ctx.Req.AddCookie(&http.Cookie{Name: "redirect_to", Value: "/other-cookie"})
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, "/other-cookie", test.RedirectURL(resp))
+
+ ctx, resp = contexttest.MockContext(t, "/user/login?redirect_to="+url.QueryEscape("https://example.com"))
+ ctx.IsSigned = true
+ SignIn(ctx)
+ assert.Equal(t, "/", test.RedirectURL(resp))
+}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 1d94e52fe3..f744a57a43 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -12,13 +12,13 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 07140b6674..3189d1372e 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"html"
+ "html/template"
"io"
"net/http"
"net/url"
@@ -21,7 +22,6 @@ import (
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@@ -33,6 +33,7 @@ import (
auth_service "code.gitea.io/gitea/services/auth"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms"
user_service "code.gitea.io/gitea/services/user"
@@ -499,11 +500,11 @@ func AuthorizeOAuth(ctx *context.Context) {
ctx.Data["Scope"] = form.Scope
ctx.Data["Nonce"] = form.Nonce
if user != nil {
- ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`@%s`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name))
+ ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`@%s`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)))
} else {
- ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`%s`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName))
+ ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`%s`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)))
}
- ctx.Data["ApplicationRedirectDomainHTML"] = "" + html.EscapeString(form.RedirectURI) + ""
+ ctx.Data["ApplicationRedirectDomainHTML"] = template.HTML("" + html.EscapeString(form.RedirectURI) + "")
// TODO document SESSION <=> FORM
err = ctx.Session.Set("client_id", app.ClientID)
if err != nil {
@@ -579,16 +580,8 @@ func GrantApplicationOAuth(ctx *context.Context) {
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) {
- t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil)
- if err != nil {
- ctx.ServerError("unable to find template", err)
- return
- }
- ctx.Resp.Header().Set("Content-Type", "application/json")
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
- if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
- ctx.ServerError("unable to execute template", err)
- }
+ ctx.JSONTemplate("user/auth/oidc_wellknown")
}
// OIDCKeys generates the JSON Web Key Set
@@ -986,7 +979,7 @@ func SignInOAuthCallback(ctx *context.Context) {
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
+ IsActive: optional.Some(!setting.OAuth2Client.RegisterEmailConfirm && !setting.Service.RegisterManualConfirm),
}
source := authSource.Cfg.(*oauth2.Source)
@@ -1164,7 +1157,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
middleware.DeleteRedirectToCookie(ctx.Resp)
- ctx.RedirectToFirst(redirectTo)
+ ctx.RedirectToCurrentSite(redirectTo)
return
}
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index 29ef772b1c..2143b8096a 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -11,12 +11,12 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 5af1696a64..0e88fe68f9 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -12,14 +12,13 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
@@ -37,7 +36,7 @@ func ForgotPasswd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil {
- log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin"))
+ log.Warn("no mail service configured")
ctx.Data["IsResetDisable"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
return
@@ -204,7 +203,7 @@ func ResetPasswdPost(ctx *context.Context) {
Password: optional.Some(ctx.FormString("password")),
MustChangePassword: optional.Some(false),
}
- if err := user_service.UpdateAuth(ctx, ctx.Doer, opts); err != nil {
+ if err := user_service.UpdateAuth(ctx, u, opts); err != nil {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Password"] = true
switch {
@@ -215,7 +214,6 @@ func ResetPasswdPost(ctx *context.Context) {
case errors.Is(err, password.ErrIsPwned):
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplResetPassword, nil)
case password.IsErrIsPwnedRequest(err):
- log.Error("%s", err.Error())
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
default:
ctx.ServerError("UpdateAuth", err)
@@ -299,7 +297,6 @@ func MustChangePasswordPost(ctx *context.Context) {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned"), tplMustChangePassword, &form)
case password.IsErrIsPwnedRequest(err):
- log.Error("%s", err.Error())
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
default:
@@ -312,9 +309,9 @@ func MustChangePasswordPost(ctx *context.Context) {
log.Trace("User updated password: %s", ctx.Doer.Name)
- if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 && !utils.IsExternalURL(redirectTo) {
+ if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" {
middleware.DeleteRedirectToCookie(ctx.Resp)
- ctx.RedirectToFirst(redirectTo)
+ ctx.RedirectToCurrentSite(redirectTo)
return
}
diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go
index 95c8d262a5..1079f44a08 100644
--- a/routers/web/auth/webauthn.go
+++ b/routers/web/auth/webauthn.go
@@ -11,9 +11,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/externalaccount"
"github.com/go-webauthn/webauthn/protocol"
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 525ca9be53..dd20663f94 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -10,8 +10,8 @@ import (
"time"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
)
// List all devtest templates, they will be used for e2e tests for the UI components
diff --git a/routers/web/events/events.go b/routers/web/events/events.go
index 1a5a162c1a..52f20e07dc 100644
--- a/routers/web/events/events.go
+++ b/routers/web/events/events.go
@@ -7,11 +7,11 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/routers/web/auth"
+ "code.gitea.io/gitea/services/context"
)
// Events listens for events
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index d81884ec62..ecd7c33e01 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -6,11 +6,12 @@ package explore
import (
"net/http"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -34,12 +35,11 @@ func Code(ctx *context.Context) {
language := ctx.FormTrim("l")
keyword := ctx.FormTrim("q")
- queryType := ctx.FormTrim("t")
- isMatch := queryType == "match"
+ isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
ctx.Data["Keyword"] = keyword
ctx.Data["Language"] = language
- ctx.Data["queryType"] = queryType
+ ctx.Data["IsFuzzy"] = isFuzzy
ctx.Data["PageIsViewCode"] = true
if keyword == "" {
@@ -77,7 +77,16 @@ func Code(ctx *context.Context) {
)
if (len(repoIDs) > 0) || isAdmin {
- total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+ total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+ RepoIDs: repoIDs,
+ Keyword: keyword,
+ IsKeywordFuzzy: isFuzzy,
+ Language: language,
+ Paginator: &db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.RepoSearchPagingNum,
+ },
+ })
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.ServerError("SearchResults", err)
@@ -128,7 +137,7 @@ func Code(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "l", "Language")
+ pager.AddParamString("l", language)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplExploreCode)
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index dc1318beef..f8fd6ec38e 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -6,9 +6,10 @@ package explore
import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
)
// Organizations render explore organizations page
@@ -24,8 +25,16 @@ func Organizations(ctx *context.Context) {
visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
}
- if ctx.FormString("sort") == "" {
- ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+ supportedSortOrders := container.SetOf(
+ "newest",
+ "oldest",
+ "alphabetically",
+ "reversealphabetically",
+ )
+ sortOrder := ctx.FormString("sort")
+ if sortOrder == "" {
+ sortOrder = "newest"
+ ctx.SetFormString("sort", sortOrder)
}
RenderUserSearch(ctx, &user_model.SearchUserOptions{
@@ -33,5 +42,7 @@ func Organizations(ctx *context.Context) {
Type: user_model.UserTypeOrganization,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
Visible: visibleTypes,
+
+ SupportedSortOrders: supportedSortOrders,
}, tplExploreUsers)
}
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index 0446edebe6..66477a255c 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -10,10 +10,10 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sitemap"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -109,6 +109,21 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
language := ctx.FormTrim("language")
ctx.Data["Language"] = language
+ archived := ctx.FormOptionalBool("archived")
+ ctx.Data["IsArchived"] = archived
+
+ fork := ctx.FormOptionalBool("fork")
+ ctx.Data["IsFork"] = fork
+
+ mirror := ctx.FormOptionalBool("mirror")
+ ctx.Data["IsMirror"] = mirror
+
+ template := ctx.FormOptionalBool("template")
+ ctx.Data["IsTemplate"] = template
+
+ private := ctx.FormOptionalBool("private")
+ ctx.Data["IsPrivate"] = private
+
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
@@ -125,6 +140,11 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
OnlyShowRelevant: opts.OnlyShowRelevant,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -149,8 +169,8 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
pager := context.NewPagination(int(count), opts.PageSize, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "topic", "TopicOnly")
- pager.AddParam(ctx, "language", "Language")
+ pager.AddParamString("topic", fmt.Sprint(topicOnly))
+ pager.AddParamString("language", language)
pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant))
ctx.Data["Page"] = pager
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index bb1be310de..b4507ba28d 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -8,8 +8,8 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -23,7 +23,7 @@ func TopicSearch(ctx *context.Context) {
},
}
- topics, total, err := repo_model.FindTopics(ctx, opts)
+ topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError)
return
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index 09d31f95ef..b79a79fb2c 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -10,12 +10,13 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sitemap"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -79,10 +80,16 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
fallthrough
default:
// in case the sortType is not valid, we set it to recentupdate
+ sortOrder = "recentupdate"
ctx.Data["SortType"] = "recentupdate"
orderBy = "`user`.updated_unix DESC"
}
+ if opts.SupportedSortOrders != nil && !opts.SupportedSortOrders.Contains(sortOrder) {
+ ctx.NotFound("unsupported sort order", nil)
+ return
+ }
+
opts.Keyword = ctx.FormTrim("q")
opts.OrderBy = orderBy
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
@@ -132,15 +139,25 @@ func Users(ctx *context.Context) {
ctx.Data["PageIsExploreUsers"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- if ctx.FormString("sort") == "" {
- ctx.SetFormString("sort", setting.UI.ExploreDefaultSort)
+ supportedSortOrders := container.SetOf(
+ "newest",
+ "oldest",
+ "alphabetically",
+ "reversealphabetically",
+ )
+ sortOrder := ctx.FormString("sort")
+ if sortOrder == "" {
+ sortOrder = "newest"
+ ctx.SetFormString("sort", sortOrder)
}
RenderUserSearch(ctx, &user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
+
+ SupportedSortOrders: supportedSortOrders,
}, tplExploreUsers)
}
diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index f13038ff9b..80ce2ad198 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -9,7 +9,7 @@ import (
"time"
"code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 66b01d3680..3defa436a7 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -6,6 +6,7 @@ package feed
import (
"fmt"
"html"
+ "html/template"
"net/http"
"net/url"
"strconv"
@@ -13,12 +14,12 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
@@ -49,7 +50,7 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
// renderMarkdown creates a minimal markdown render context from an action.
// If rendering fails, the original markdown text is returned
-func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
+func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML {
markdownCtx := &markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
@@ -63,7 +64,7 @@ func renderMarkdown(ctx *context.Context, act *activities_model.Action, content
}
markdown, err := markdown.RenderString(markdownCtx, content)
if err != nil {
- return content
+ return templates.SanitizeHTML(content) // old code did so: use SanitizeHTML to render in tmpl
}
return markdown
}
@@ -73,125 +74,130 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
for _, act := range actions {
act.LoadActUser(ctx)
- var content, desc, title string
+ // TODO: the code seems quite strange (maybe not right)
+ // sometimes it uses text content but sometimes it uses HTML content
+ // it should clearly defines which kind of content it should use for the feed items: plan text or rich HTML
+ var title, desc string
+ var content template.HTML
link := &feeds.Link{Href: act.GetCommentHTMLURL(ctx)}
// title
title = act.ActUser.DisplayName() + " "
+ var titleExtra template.HTML
switch act.OpType {
case activities_model.ActionCreateRepo:
- title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionRenameRepo:
- title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionCommitRepo:
link.Href = toBranchLink(ctx, act)
if len(act.Content) != 0 {
- title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
} else {
- title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
}
case activities_model.ActionCreateIssue:
link.Href = toIssueLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCreatePullRequest:
link.Href = toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionTransferRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
case activities_model.ActionPushTag:
link.Href = toTagLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionCommentIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
- title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionMergePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionAutoMergePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCloseIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
- title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenIssue:
issueLink := toIssueLink(ctx, act)
if link.Href == "#" {
link.Href = issueLink
}
- title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionClosePullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenPullRequest:
pullLink := toPullLink(ctx, act)
if link.Href == "#" {
link.Href = pullLink
}
- title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionDeleteTag:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionDeleteBranch:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncPush:
srcLink := toSrcLink(ctx, act)
if link.Href == "#" {
link.Href = srcLink
}
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncCreate:
srcLink := toSrcLink(ctx, act)
if link.Href == "#" {
link.Href = srcLink
}
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncDelete:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionApprovePullRequest:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionRejectPullRequest:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCommentPull:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionPublishRelease:
releaseLink := toReleaseLink(ctx, act)
if link.Href == "#" {
link.Href = releaseLink
}
- title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
+ titleExtra = ctx.Locale.Tr("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
case activities_model.ActionPullReviewDismissed:
pullLink := toPullLink(ctx, act)
- title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
+ titleExtra = ctx.Locale.Tr("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
case activities_model.ActionStarRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
case activities_model.ActionWatchRepo:
link.Href = act.GetRepoAbsoluteLink(ctx)
- title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
+ titleExtra = ctx.Locale.Tr("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
default:
return nil, fmt.Errorf("unknown action type: %v", act.OpType)
}
@@ -226,22 +232,22 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
desc = act.GetIssueTitle(ctx)
comment := act.GetIssueInfos()[1]
if len(comment) != 0 {
- desc += "\n\n" + renderMarkdown(ctx, act, comment)
+ desc += "\n\n" + string(renderMarkdown(ctx, act, comment))
}
case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
desc = act.GetIssueInfos()[1]
case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest:
desc = act.GetIssueTitle(ctx)
case activities_model.ActionPullReviewDismissed:
- desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
+ desc = ctx.Locale.TrString("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
}
}
if len(content) == 0 {
- content = desc
+ content = templates.SanitizeHTML(desc)
}
items = append(items, &feeds.Item{
- Title: title,
+ Title: template.HTMLEscapeString(title) + string(titleExtra),
Link: link,
Description: desc,
IsPermaLink: "false",
@@ -251,7 +257,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
},
Id: fmt.Sprintf("%v: %v", strconv.FormatInt(act.ID, 10), link.Href),
Created: act.CreatedUnix.AsTime(),
- Content: content,
+ Content: string(content),
})
}
return items, err
@@ -280,7 +286,8 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
return nil, err
}
- var title, content string
+ var title string
+ var content template.HTML
if rel.IsTag {
title = rel.TagName
@@ -309,7 +316,7 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
Email: rel.Publisher.GetEmail(),
},
Id: fmt.Sprintf("%v: %v", strconv.FormatInt(rel.ID, 10), link.Href),
- Content: content,
+ Content: string(content),
})
}
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
index 56a9c54ddc..1ab768ff27 100644
--- a/routers/web/feed/file.go
+++ b/routers/web/feed/file.go
@@ -9,9 +9,9 @@ import (
"time"
"code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 04f84c0c8d..08cbcd9e12 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -7,9 +7,9 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
@@ -56,9 +56,9 @@ func showUserFeed(ctx *context.Context, formatType string) {
}
feed := &feeds.Feed{
- Title: ctx.Tr("home.feed_of", ctx.ContextUser.DisplayName()),
+ Title: ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()),
Link: &feeds.Link{Href: ctx.ContextUser.HTMLURL()},
- Description: ctxUserDescription,
+ Description: string(ctxUserDescription),
Created: time.Now(),
}
diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go
index 57b0c92766..273f47e3b4 100644
--- a/routers/web/feed/release.go
+++ b/routers/web/feed/release.go
@@ -8,7 +8,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
@@ -28,10 +28,10 @@ func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleas
var link *feeds.Link
if isReleasesOnly {
- title = ctx.Tr("repo.release.releases_for", repo.FullName())
+ title = ctx.Locale.TrString("repo.release.releases_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/release"}
} else {
- title = ctx.Tr("repo.release.tags_for", repo.FullName())
+ title = ctx.Locale.TrString("repo.release.tags_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/tags"}
}
diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go
index 8931dae8cc..a41808c24a 100644
--- a/routers/web/feed/render.go
+++ b/routers/web/feed/render.go
@@ -4,7 +4,7 @@
package feed
import (
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
// RenderBranchFeed render format for branch or file
diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go
index 5fcad26779..bfcc3a37d6 100644
--- a/routers/web/feed/repo.go
+++ b/routers/web/feed/repo.go
@@ -8,7 +8,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds"
)
@@ -27,7 +27,7 @@ func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType
}
feed := &feeds.Feed{
- Title: ctx.Tr("home.feed_of", repo.FullName()),
+ Title: ctx.Locale.TrString("home.feed_of", repo.FullName()),
Link: &feeds.Link{Href: repo.HTMLURL()},
Description: repo.Description,
Created: time.Now(),
diff --git a/routers/web/githttp.go b/routers/web/githttp.go
index ab74e9a333..5f1dedce76 100644
--- a/routers/web/githttp.go
+++ b/routers/web/githttp.go
@@ -6,11 +6,10 @@ package web
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
- context_service "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context"
)
func requireSignIn(ctx *context.Context) {
@@ -39,5 +38,5 @@ func gitHTTPRouters(m *web.Route) {
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
- }, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context_service.UserAssignmentWeb())
+ }, ignSignInAndCsrf, requireSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
}
diff --git a/routers/web/goget.go b/routers/web/goget.go
index c5b8b6cbc0..8d5612ebfe 100644
--- a/routers/web/goget.go
+++ b/routers/web/goget.go
@@ -12,9 +12,9 @@ import (
"strings"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
func goGet(ctx *context.Context) {
diff --git a/routers/web/home.go b/routers/web/home.go
index 2321b00efe..d4be0931e8 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -12,15 +12,15 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sitemap"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/routers/web/user"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -71,7 +71,7 @@ func HomeSitemap(ctx *context.Context) {
_, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: 1},
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
Visible: []structs.VisibleType{structs.VisibleTypePublic},
})
if err != nil {
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index c91da9a7f1..2dbbd6fc09 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -5,10 +5,10 @@
package misc
import (
- "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/context"
)
// Markup render markup document to HTML
diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go
index 54c93763f6..ac5496ce91 100644
--- a/routers/web/misc/misc.go
+++ b/routers/web/misc/misc.go
@@ -15,7 +15,7 @@ import (
)
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
- if !git.SupportProcReceive {
+ if !git.DefaultFeatures.SupportProcReceive {
rw.WriteHeader(http.StatusNotFound)
return
}
diff --git a/routers/web/misc/swagger.go b/routers/web/misc/swagger.go
index 72c09a3780..5fddfa8885 100644
--- a/routers/web/misc/swagger.go
+++ b/routers/web/misc/swagger.go
@@ -7,7 +7,7 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
// tplSwagger swagger page template
diff --git a/routers/web/nodeinfo.go b/routers/web/nodeinfo.go
index 01b71e7086..f1cc7bf530 100644
--- a/routers/web/nodeinfo.go
+++ b/routers/web/nodeinfo.go
@@ -7,8 +7,8 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
type nodeInfoLinks struct {
diff --git a/routers/web/org/block.go b/routers/web/org/block.go
new file mode 100644
index 0000000000..d40458e250
--- /dev/null
+++ b/routers/web/org/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsBlockedUsers base.TplName = "org/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("user.block.list")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+ shared_user.BlockedUsers(ctx, ctx.ContextUser)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+ shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
+}
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 8bf02b2c42..846b1de18a 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -11,9 +11,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
@@ -21,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -46,17 +45,6 @@ func Home(ctx *context.Context) {
ctx.Data["PageIsUserProfile"] = true
ctx.Data["Title"] = org.DisplayName()
- if len(org.Description) != 0 {
- desc, err := markdown.RenderString(&markup.RenderContext{
- Ctx: ctx,
- Metas: map[string]string{"mode": "document"},
- }, org.Description)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
- }
- ctx.Data["RenderedDescription"] = desc
- }
var orderBy db.SearchOrderBy
ctx.Data["SortType"] = ctx.FormString("sort")
@@ -97,6 +85,21 @@ func Home(ctx *context.Context) {
page = 1
}
+ archived := ctx.FormOptionalBool("archived")
+ ctx.Data["IsArchived"] = archived
+
+ fork := ctx.FormOptionalBool("fork")
+ ctx.Data["IsFork"] = fork
+
+ mirror := ctx.FormOptionalBool("mirror")
+ ctx.Data["IsMirror"] = mirror
+
+ template := ctx.FormOptionalBool("template")
+ ctx.Data["IsTemplate"] = template
+
+ private := ctx.FormOptionalBool("private")
+ ctx.Data["IsPrivate"] = private
+
var (
repos []*repo_model.Repository
count int64
@@ -114,6 +117,11 @@ func Home(ctx *context.Context) {
Actor: ctx.Doer,
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -131,18 +139,12 @@ func Home(ctx *context.Context) {
return
}
- var isFollowing bool
- if ctx.Doer != nil {
- isFollowing = user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
- }
-
ctx.Data["Repos"] = repos
ctx.Data["Total"] = count
ctx.Data["Members"] = members
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["PageIsViewRepositories"] = true
- ctx.Data["IsFollowing"] = isFollowing
err = shared_user.LoadHeaderCount(ctx)
if err != nil {
@@ -152,7 +154,7 @@ func Home(ctx *context.Context) {
pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "language", "Language")
+ pager.AddParamString("language", language)
ctx.Data["Page"] = pager
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 15a615c706..63ac57cf0d 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -9,11 +9,12 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -78,40 +79,43 @@ func Members(ctx *context.Context) {
// MembersAction response for operation to a member of organization
func MembersAction(ctx *context.Context) {
- uid := ctx.FormInt64("uid")
- if uid == 0 {
+ member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+ if err != nil {
+ log.Error("GetUserByID: %v", err)
+ }
+ if member == nil {
ctx.Redirect(ctx.Org.OrgLink + "/members")
return
}
org := ctx.Org.Organization
- var err error
+
switch ctx.Params(":action") {
case "private":
- if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, false)
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
case "public":
- if ctx.Doer.ID != uid && !ctx.Org.IsOwner {
+ if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = organization.ChangeOrgUserStatus(ctx, org.ID, uid, true)
+ err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
case "remove":
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
- err = models.RemoveOrgUser(ctx, org.ID, uid)
+ err = models.RemoveOrgUser(ctx, org, member)
if organization.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
return
}
case "leave":
- err = models.RemoveOrgUser(ctx, org.ID, ctx.Doer.ID)
+ err = models.RemoveOrgUser(ctx, org, ctx.Doer)
if err == nil {
ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
index 52f8df8a1c..f94dd16eae 100644
--- a/routers/web/org/org.go
+++ b/routers/web/org/org.go
@@ -12,10 +12,10 @@ import (
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -29,7 +29,7 @@ func Create(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
if !ctx.Doer.CanCreateOrganization() {
- ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+ ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return
}
ctx.HTML(http.StatusOK, tplCreateOrg)
@@ -41,7 +41,7 @@ func CreatePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
if !ctx.Doer.CanCreateOrganization() {
- ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed")))
+ ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return
}
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index f78bd00274..02eae8052e 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -8,10 +8,10 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 03798a712c..596a370d2e 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -17,12 +17,13 @@ import (
attachment_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -66,7 +67,7 @@ func Projects(ctx *context.Context) {
PageSize: setting.UI.IssuePagingNum,
},
OwnerID: ctx.ContextUser.ID,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: optional.Some(isShowClosed),
OrderBy: project_model.GetSearchOrderByBySortType(sortType),
Type: projectType,
Title: keyword,
@@ -78,7 +79,7 @@ func Projects(ctx *context.Context) {
opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
OwnerID: ctx.ContextUser.ID,
- IsClosed: util.OptionalBoolOf(!isShowClosed),
+ IsClosed: optional.Some(!isShowClosed),
Type: projectType,
})
if err != nil {
@@ -104,7 +105,7 @@ func Projects(ctx *context.Context) {
}
for _, project := range projects {
- project.RenderedContent = project.Description
+ project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
}
err = shared_user.LoadHeaderCount(ctx)
@@ -119,7 +120,7 @@ func Projects(ctx *context.Context) {
}
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
- pager.AddParam(ctx, "state", "State")
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
@@ -206,11 +207,7 @@ func ChangeProjectStatus(ctx *context.Context) {
id := ctx.ParamsInt64(":id")
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", err)
- } else {
- ctx.ServerError("ChangeProjectStatusByRepoIDAndID", err)
- }
+ ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
return
}
ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects?state=" + url.QueryEscape(ctx.Params(":action")))
@@ -220,11 +217,7 @@ func ChangeProjectStatus(ctx *context.Context) {
func DeleteProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
if p.OwnerID != ctx.ContextUser.ID {
@@ -253,11 +246,7 @@ func RenderEditProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
if p.OwnerID != ctx.ContextUser.ID {
@@ -302,11 +291,7 @@ func EditProjectPost(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
if p.OwnerID != ctx.ContextUser.ID {
@@ -334,11 +319,7 @@ func EditProjectPost(ctx *context.Context) {
func ViewProject(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
if project.OwnerID != ctx.ContextUser.ID {
@@ -352,10 +333,6 @@ func ViewProject(ctx *context.Context) {
return
}
- if boards[0].ID == 0 {
- boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
- }
-
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err)
@@ -377,17 +354,17 @@ func ViewProject(ctx *context.Context) {
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
- var referencedIds []int64
+ var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
- referencedIds = append(referencedIds, comment.RefIssueID)
+ referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
- if len(referencedIds) > 0 {
+ if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
- IssueIDs: referencedIds,
- IsPull: util.OptionalBoolTrue,
+ IssueIDs: referencedIDs,
+ IsPull: optional.Some(true),
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
@@ -395,7 +372,7 @@ func ViewProject(ctx *context.Context) {
}
}
- project.RenderedContent = project.Description
+ project.RenderedContent = templates.SanitizeHTML(project.Description) // FIXME: is it right? why not render?
ctx.Data["LinkedPRs"] = linkedPrsMap
ctx.Data["PageIsViewProjects"] = true
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
@@ -492,11 +469,7 @@ func DeleteProjectBoard(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
@@ -533,11 +506,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
@@ -565,11 +534,7 @@ func CheckProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return nil, nil
}
@@ -635,21 +600,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
ctx.JSONOK()
}
-// UnsetDefaultProjectBoard unset default board for uncategorized issues/pulls
-func UnsetDefaultProjectBoard(ctx *context.Context) {
- project, _ := CheckProjectBoardChangePermissions(ctx)
- if ctx.Written() {
- return
- }
-
- if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
- ctx.ServerError("SetDefaultBoard", err)
- return
- }
-
- ctx.JSONOK()
-}
-
// MoveIssues moves or keeps issues in a column and sorts them inside that column
func MoveIssues(ctx *context.Context) {
if ctx.Doer == nil {
@@ -661,11 +611,7 @@ func MoveIssues(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("ProjectNotExist", nil)
- } else {
- ctx.ServerError("GetProjectByID", err)
- }
+ ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return
}
if project.OwnerID != ctx.ContextUser.ID {
@@ -673,28 +619,15 @@ func MoveIssues(ctx *context.Context) {
return
}
- var board *project_model.Board
+ board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetProjectBoard", project_model.IsErrProjectBoardNotExist, err)
+ return
+ }
- if ctx.ParamsInt64(":boardID") == 0 {
- board = &project_model.Board{
- ID: 0,
- ProjectID: project.ID,
- Title: ctx.Tr("repo.projects.type.uncategorized"),
- }
- } else {
- board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
- if err != nil {
- if project_model.IsErrProjectBoardNotExist(err) {
- ctx.NotFound("ProjectBoardNotExist", nil)
- } else {
- ctx.ServerError("GetProjectBoard", err)
- }
- return
- }
- if board.ProjectID != project.ID {
- ctx.NotFound("BoardNotInProject", nil)
- return
- }
+ if board.ProjectID != project.ID {
+ ctx.NotFound("BoardNotInProject", nil)
+ return
}
type movedIssuesForm struct {
@@ -717,11 +650,7 @@ func MoveIssues(ctx *context.Context) {
}
movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
- if issues_model.IsErrIssueNotExist(err) {
- ctx.NotFound("IssueNotExisting", nil)
- } else {
- ctx.ServerError("GetIssueByID", err)
- }
+ ctx.NotFoundOrServerError("GetIssueByID", issues_model.IsErrIssueNotExist, err)
return
}
diff --git a/routers/web/org/projects_test.go b/routers/web/org/projects_test.go
index 8053ab4cf9..f4ccfe1c06 100644
--- a/routers/web/org/projects_test.go
+++ b/routers/web/org/projects_test.go
@@ -7,8 +7,8 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/routers/web/org"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 47d0063f76..494ada4323 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -14,7 +14,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -22,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/web/org/setting/runners.go b/routers/web/org/setting/runners.go
index c3c771036a..fe05709237 100644
--- a/routers/web/org/setting/runners.go
+++ b/routers/web/org/setting/runners.go
@@ -4,7 +4,7 @@
package setting
import (
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go
index ca4fe09f38..7f855795d3 100644
--- a/routers/web/org/setting_oauth2.go
+++ b/routers/web/org/setting_oauth2.go
@@ -10,10 +10,10 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go
index 796829d34e..af9836e42c 100644
--- a/routers/web/org/setting_packages.go
+++ b/routers/web/org/setting_packages.go
@@ -8,10 +8,10 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/packages"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 71fe99c97c..144d9b1b43 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -5,6 +5,7 @@
package org
import (
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -20,11 +21,11 @@ import (
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
org_service "code.gitea.io/gitea/services/org"
@@ -77,9 +78,9 @@ func TeamsAction(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
return
}
- err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ err = models.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
case "leave":
- err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer.ID)
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, ctx.Doer)
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -100,13 +101,13 @@ func TeamsAction(ctx *context.Context) {
return
}
- uid := ctx.FormInt64("uid")
- if uid == 0 {
+ user, _ := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
+ if user == nil {
ctx.Redirect(ctx.Org.OrgLink + "/teams")
return
}
- err = models.RemoveTeamMember(ctx, ctx.Org.Team, uid)
+ err = models.RemoveTeamMember(ctx, ctx.Org.Team, user)
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
@@ -161,7 +162,7 @@ func TeamsAction(ctx *context.Context) {
if ctx.Org.Team.IsMember(ctx, u.ID) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
- err = models.AddTeamMember(ctx, ctx.Org.Team, u.ID)
+ err = models.AddTeamMember(ctx, ctx.Org.Team, u)
}
page = "team"
@@ -189,6 +190,8 @@ func TeamsAction(ctx *context.Context) {
if err != nil {
if org_model.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Flash.Error(ctx.Tr("org.teams.members.blocked_user"))
} else {
log.Error("Action(%s): %v", ctx.Params(":action"), err)
ctx.JSON(http.StatusOK, map[string]any{
@@ -590,7 +593,7 @@ func TeamInvitePost(ctx *context.Context) {
return
}
- if err := models.AddTeamMember(ctx, team, ctx.Doer.ID); err != nil {
+ if err := models.AddTeamMember(ctx, team, ctx.Doer); err != nil {
ctx.ServerError("AddTeamMember", err)
return
}
diff --git a/routers/web/passkey.go b/routers/web/passkey.go
new file mode 100644
index 0000000000..0d10a69dfe
--- /dev/null
+++ b/routers/web/passkey.go
@@ -0,0 +1,24 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package web
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
+)
+
+type passkeyEndpointsType struct {
+ Enroll string `json:"enroll"`
+ Manage string `json:"manage"`
+}
+
+func passkeyEndpoints(ctx *context.Context) {
+ url := setting.AppURL + "user/settings/security"
+ ctx.JSON(http.StatusOK, passkeyEndpointsType{
+ Enroll: url,
+ Manage: url,
+ })
+}
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index fe528a483b..6059ad1414 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -15,11 +15,11 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/repo"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"github.com/nektos/act/pkg/model"
@@ -61,24 +61,24 @@ func List(ctx *context.Context) {
var workflows []Workflow
if empty, err := ctx.Repo.GitRepo.IsEmpty(); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("IsEmpty", err)
return
} else if !empty {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetBranchCommit", err)
return
}
entries, err := actions.ListWorkflows(commit)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("ListWorkflows", err)
return
}
// Get all runner labels
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
RepoID: ctx.Repo.Repository.ID,
- IsOnline: util.OptionalBoolTrue,
+ IsOnline: optional.Some(true),
WithAvailable: true,
})
if err != nil {
@@ -95,17 +95,22 @@ func List(ctx *context.Context) {
workflow := Workflow{Entry: *entry}
content, err := actions.GetContentFromEntry(entry)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetContentFromEntry", err)
return
}
wf, err := model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
- workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error())
+ workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow)
continue
}
- // Check whether have matching runner
+ // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
+ hasJobWithoutNeeds := false
+ // Check whether have matching runner and a job without "needs"
for _, j := range wf.Jobs {
+ if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
+ hasJobWithoutNeeds = true
+ }
runsOnList := j.RunsOn()
for _, ro := range runsOnList {
if strings.Contains(ro, "${{") {
@@ -115,7 +120,7 @@ func List(ctx *context.Context) {
continue
}
if !allRunnerLabels.Contains(ro) {
- workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_online_runner_helper", ro)
+ workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
break
}
}
@@ -123,6 +128,9 @@ func List(ctx *context.Context) {
break
}
}
+ if !hasJobWithoutNeeds {
+ workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
+ }
workflows = append(workflows, workflow)
}
}
@@ -172,7 +180,7 @@ func List(ctx *context.Context) {
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("FindAndCount", err)
return
}
@@ -181,7 +189,7 @@ func List(ctx *context.Context) {
}
if err := actions_model.RunList(runs).LoadTriggerUser(ctx); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("LoadTriggerUser", err)
return
}
@@ -189,7 +197,7 @@ func List(ctx *context.Context) {
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetActors", err)
return
}
ctx.Data["Actors"] = repo.MakeSelfOnTop(ctx.Doer, actors)
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
new file mode 100644
index 0000000000..6fa951826c
--- /dev/null
+++ b/routers/web/repo/actions/badge.go
@@ -0,0 +1,56 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/badge"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+)
+
+func GetWorkflowBadge(ctx *context.Context) {
+ workflowFile := ctx.Params("workflow_name")
+ branch := ctx.Req.URL.Query().Get("branch")
+ if branch == "" {
+ branch = ctx.Repo.Repository.DefaultBranch
+ }
+ branchRef := fmt.Sprintf("refs/heads/%s", branch)
+ event := ctx.Req.URL.Query().Get("event")
+
+ badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+ if err != nil {
+ ctx.ServerError("GetWorkflowBadge", err)
+ return
+ }
+
+ ctx.Data["Badge"] = badge
+ ctx.RespHeader().Set("Content-Type", "image/svg+xml")
+ ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+}
+
+func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
+ extension := filepath.Ext(workflowFile)
+ workflowName := strings.TrimSuffix(workflowFile, extension)
+
+ run, err := actions_model.GetWorkflowLatestRun(ctx, ctx.Repo.Repository.ID, workflowFile, branchName, event)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ return badge.GenerateBadge(workflowName, "no status", badge.DefaultColor), nil
+ }
+ return badge.Badge{}, err
+ }
+
+ color, ok := badge.StatusColorMap[run.Status]
+ if !ok {
+ return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
+ }
+ return badge.GenerateBadge(workflowName, run.Status.String(), color), nil
+}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index 9cda30d23d..41989589be 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -12,6 +12,7 @@ import (
"io"
"net/http"
"net/url"
+ "strconv"
"strings"
"time"
@@ -21,12 +22,13 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
- context_module "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
+ context_module "code.gitea.io/gitea/services/context"
"xorm.io/builder"
)
@@ -57,15 +59,16 @@ type ViewRequest struct {
type ViewResponse struct {
State struct {
Run struct {
- Link string `json:"link"`
- Title string `json:"title"`
- Status string `json:"status"`
- CanCancel bool `json:"canCancel"`
- CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
- CanRerun bool `json:"canRerun"`
- Done bool `json:"done"`
- Jobs []*ViewJob `json:"jobs"`
- Commit ViewCommit `json:"commit"`
+ Link string `json:"link"`
+ Title string `json:"title"`
+ Status string `json:"status"`
+ CanCancel bool `json:"canCancel"`
+ CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
+ CanRerun bool `json:"canRerun"`
+ CanDeleteArtifact bool `json:"canDeleteArtifact"`
+ Done bool `json:"done"`
+ Jobs []*ViewJob `json:"jobs"`
+ Commit ViewCommit `json:"commit"`
} `json:"run"`
CurrentJob struct {
Title string `json:"title"`
@@ -146,6 +149,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
+ resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.Done = run.Status.IsDone()
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
resp.State.Run.Status = run.Status.String()
@@ -168,8 +172,8 @@ func ViewPost(ctx *context_module.Context) {
Link: run.RefLink(),
}
resp.State.Run.Commit = ViewCommit{
- LocaleCommit: ctx.Tr("actions.runs.commit"),
- LocalePushedBy: ctx.Tr("actions.runs.pushed_by"),
+ LocaleCommit: ctx.Locale.TrString("actions.runs.commit"),
+ LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"),
ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
Pusher: pusher,
@@ -194,7 +198,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Title = current.Name
resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
if run.NeedApproval {
- resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc")
+ resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc")
}
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
@@ -260,10 +264,14 @@ func ViewPost(ctx *context_module.Context) {
}
// Rerun will rerun jobs in the given run
-// jobIndex = 0 means rerun all jobs
+// If jobIndexStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
runIndex := ctx.ParamsInt64("run")
- jobIndex := ctx.ParamsInt64("job")
+ jobIndexStr := ctx.Params("job")
+ var jobIndex int64
+ if jobIndexStr != "" {
+ jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
+ }
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
@@ -295,12 +303,25 @@ func Rerun(ctx *context_module.Context) {
return
}
- if jobIndex != 0 {
- jobs = []*actions_model.ActionRunJob{job}
+ if jobIndexStr == "" { // rerun all jobs
+ for _, j := range jobs {
+ // if the job has needs, it should be set to "blocked" status to wait for other jobs
+ shouldBlock := len(j.Needs) > 0
+ if err := rerunJob(ctx, j, shouldBlock); err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+ ctx.JSON(http.StatusOK, struct{}{})
+ return
}
- for _, j := range jobs {
- if err := rerunJob(ctx, j); err != nil {
+ rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
+
+ for _, j := range rerunJobs {
+ // jobs other than the specified one should be set to "blocked" status
+ shouldBlock := j.JobID != job.JobID
+ if err := rerunJob(ctx, j, shouldBlock); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
@@ -309,7 +330,7 @@ func Rerun(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, struct{}{})
}
-func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
+func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
status := job.Status
if !status.IsDone() {
return nil
@@ -317,6 +338,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) erro
job.TaskID = 0
job.Status = actions_model.StatusWaiting
+ if shouldBlock {
+ job.Status = actions_model.StatusBlocked
+ }
job.Started = 0
job.Stopped = 0
@@ -535,6 +559,29 @@ func ArtifactsView(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, artifactsResponse)
}
+func ArtifactsDeleteView(ctx *context_module.Context) {
+ if !ctx.Repo.CanWrite(unit.TypeActions) {
+ ctx.Error(http.StatusForbidden, "no permission")
+ return
+ }
+
+ runIndex := ctx.ParamsInt64("run")
+ artifactName := ctx.Params("artifact_name")
+
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ ctx.JSON(http.StatusOK, struct{}{})
+}
+
func ArtifactsDownloadView(ctx *context_module.Context) {
runIndex := ctx.ParamsInt64("run")
artifactName := ctx.Params("artifact_name")
@@ -562,8 +609,38 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
return
}
+ // if artifacts status is not uploaded-confirmed, treat it as not found
+ for _, art := range artifacts {
+ if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
+ ctx.Error(http.StatusNotFound, "artifact not found")
+ return
+ }
+ }
+
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
+ // Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
+ // The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
+ if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
+ art := artifacts[0]
+ if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
+ u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath)
+ if u != nil && err == nil {
+ ctx.Redirect(u.String())
+ return
+ }
+ }
+ f, err := storage.ActionsArtifacts.Open(art.StoragePath)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ _, _ = io.Copy(ctx.Resp, f)
+ return
+ }
+
+ // Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
+ // Those need to be zipped for download
writer := zip.NewWriter(ctx.Resp)
defer writer.Close()
for _, art := range artifacts {
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 3d030edaca..6f6641cc65 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -10,7 +10,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -22,6 +22,8 @@ func Activity(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.activity")
ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsPulse"] = true
+
ctx.Data["Period"] = ctx.Params("period")
timeUntil := time.Now()
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 8c322b45e5..f0c5622aec 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -9,15 +9,15 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
repo_service "code.gitea.io/gitea/services/repository"
)
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index d414779a14..1887e4d95d 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -13,13 +13,15 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
)
type blameRow struct {
@@ -86,9 +88,16 @@ func RefBlame(ctx *context.Context) {
ctx.Data["IsBlame"] = true
- ctx.Data["FileSize"] = blob.Size()
+ fileSize := blob.Size()
+ ctx.Data["FileSize"] = fileSize
ctx.Data["FileName"] = blob.Name()
+ if fileSize >= setting.UI.MaxDisplayFileSize {
+ ctx.Data["IsFileTooLarge"] = true
+ ctx.HTML(http.StatusOK, tplRepoHome)
+ return
+ }
+
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
ctx.Data["NumLinesSet"] = true
@@ -131,11 +140,8 @@ type blameResult struct {
}
func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
- objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
- if err != nil {
- ctx.NotFound("CreateBlameReader", err)
- return nil, err
- }
+ objectFormat := ctx.Repo.GetObjectFormat()
+
blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore)
if err != nil {
return nil, err
@@ -247,31 +253,11 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
repoLink := ctx.Repo.RepoLink
- language := ""
-
- indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
- if err == nil {
- defer deleteTemporaryFile()
-
- filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
- CachedOnly: true,
- Attributes: []string{"linguist-language", "gitlab-language"},
- Filenames: []string{ctx.Repo.TreePath},
- IndexFile: indexFilename,
- WorkTree: worktree,
- })
- if err != nil {
- log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
- }
-
- language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
- if language == "" || language == "unspecified" {
- language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
- }
- if language == "unspecified" {
- language = ""
- }
+ language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+ if err != nil {
+ log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
+
lines := make([]string, 0)
rows := make([]*blameRow, 0)
escapeStatus := &charset.EscapeStatus{}
@@ -300,9 +286,9 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames
var avatar string
if commit.User != nil {
- avatar = string(avatarUtils.Avatar(commit.User, 18, "gt-mr-3"))
+ avatar = string(avatarUtils.Avatar(commit.User, 18))
} else {
- avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "gt-mr-3"))
+ avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "tw-mr-2"))
}
br.Avatar = gotemplate.HTML(avatar)
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index a31d611fc0..f879a98786 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -16,15 +16,15 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
@@ -54,7 +54,7 @@ func Branches(ctx *context.Context) {
kw := ctx.FormString("q")
- defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, util.OptionalBoolNone, kw, page, pageSize)
+ defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
if err != nil {
ctx.ServerError("LoadBranches", err)
return
@@ -148,12 +148,7 @@ func RestoreBranchPost(ctx *context.Context) {
return
}
- objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository)
- if err != nil {
- log.Error("RestoreBranch: CreateBranch: %w", err)
- ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
- return
- }
+ objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
// Don't return error below this
if err := repo_service.PushUpdate(
@@ -232,7 +227,7 @@ func CreateBranch(ctx *context.Context) {
if len(e.Message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.editor.push_rejected_no_message"))
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(e.Message),
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
index 25dd881219..088f8d889d 100644
--- a/routers/web/repo/cherry_pick.go
+++ b/routers/web/repo/cherry_pick.go
@@ -12,11 +12,11 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/repository/files"
)
@@ -104,9 +104,9 @@ func CherryPickPost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary)
if message == "" {
if form.Revert {
- message = ctx.Tr("repo.commit.revert-header", sha)
+ message = ctx.Locale.TrString("repo.commit.revert-header", sha)
} else {
- message = ctx.Tr("repo.commit.cherry-pick-header", sha)
+ message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
}
}
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
new file mode 100644
index 0000000000..c76f492da0
--- /dev/null
+++ b/routers/web/repo/code_frequency.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/services/context"
+ contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplCodeFrequency base.TplName = "repo/activity"
+)
+
+// CodeFrequency renders the page to show repository code frequency
+func CodeFrequency(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency")
+
+ ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsCodeFrequency"] = true
+ ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+ ctx.HTML(http.StatusOK, tplCodeFrequency)
+}
+
+// CodeFrequencyData returns JSON of code frequency data
+func CodeFrequencyData(ctx *context.Context) {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+ ctx.Status(http.StatusAccepted)
+ return
+ }
+ ctx.ServerError("GetCodeFrequencyData", err)
+ } else {
+ ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+ }
+}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 32fa973ef6..8543fa44cc 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -19,7 +19,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitgraph"
"code.gitea.io/gitea/modules/gitrepo"
@@ -27,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
git_service "code.gitea.io/gitea/services/repository"
)
@@ -163,8 +163,8 @@ func Graph(ctx *context.Context) {
ctx.Data["CommitCount"] = commitsCount
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
- paginator.AddParam(ctx, "mode", "Mode")
- paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
+ paginator.AddParamString("mode", mode)
+ paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs))
for _, branch := range branches {
paginator.AddParamString("branch", branch)
}
@@ -203,7 +203,7 @@ func SearchCommits(ctx *context.Context) {
ctx.Data["Keyword"] = query
if all {
- ctx.Data["All"] = "checked"
+ ctx.Data["All"] = true
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
@@ -351,7 +351,7 @@ func Diff(ctx *context.Context) {
ctx.Data["Commit"] = commit
ctx.Data["Diff"] = diff
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptions{ListAll: true})
+ statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index a3593815b8..cfb0e859bd 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -25,17 +25,18 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
csv_module "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/gitdiff"
)
@@ -126,7 +127,7 @@ func setCsvCompareContext(ctx *context.Context) {
return CsvDiffResult{nil, ""}
}
- errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
+ errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large"))
csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
if blob == nil {
@@ -311,14 +312,14 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
- objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat()
+
if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check if baseBranch is short sha commit hash
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
ci.BaseBranch = baseCommit.ID.String()
ctx.Data["BaseBranch"] = ci.BaseBranch
baseIsCommit = true
- } else if ci.BaseBranch == objectFormat.EmptyObjectID().String() {
+ } else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
if isSameRepo {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
} else {
@@ -696,11 +697,9 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
defer gitRepo.Close()
branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
- RepoID: repo.ID,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
- IsDeletedBranch: util.OptionalBoolFalse,
+ RepoID: repo.ID,
+ ListOptions: db.ListOptionsAll,
+ IsDeletedBranch: optional.Some(false),
})
if err != nil {
return nil, nil, err
@@ -753,11 +752,9 @@ func CompareDiff(ctx *context.Context) {
}
headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
- RepoID: ci.HeadRepo.ID,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
- IsDeletedBranch: util.OptionalBoolFalse,
+ RepoID: ci.HeadRepo.ID,
+ ListOptions: db.ListOptionsAll,
+ IsDeletedBranch: optional.Some(false),
})
if err != nil {
ctx.ServerError("GetBranches", err)
@@ -979,5 +976,8 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu
}
diffLines = append(diffLines, diffLine)
}
+ if err = scanner.Err(); err != nil {
+ return nil, fmt.Errorf("getExcerptLines scan: %w", err)
+ }
return diffLines, nil
}
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
new file mode 100644
index 0000000000..5fda17469e
--- /dev/null
+++ b/routers/web/repo/contributors.go
@@ -0,0 +1,44 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/services/context"
+ contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplContributors base.TplName = "repo/activity"
+)
+
+// Contributors render the page to show repository contributors graph
+func Contributors(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors")
+
+ ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsContributors"] = true
+
+ ctx.PageData["contributionType"] = "commits"
+
+ ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+ ctx.HTML(http.StatusOK, tplContributors)
+}
+
+// ContributorsData renders JSON of contributors along with their weekly commit statistics
+func ContributorsData(ctx *context.Context) {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+ ctx.Status(http.StatusAccepted)
+ return
+ }
+ ctx.ServerError("GetContributorStats", err)
+ } else {
+ ctx.JSON(http.StatusOK, contributorStats)
+ }
+}
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index a9e2e2b2fa..c4a8baecca 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -9,7 +9,6 @@ import (
"time"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/lfs"
@@ -17,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/context"
)
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 85d40e7820..474f7ff1da 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -16,17 +16,17 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
files_service "code.gitea.io/gitea/services/repository/files"
)
@@ -80,8 +80,12 @@ func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName,
}
}
- // Redirect to viewing file or folder
- ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(newBranchName) + "/" + util.PathEscapeSegments(treePath))
+ returnURI := ctx.FormString("return_uri")
+
+ ctx.RedirectToCurrentSite(
+ returnURI,
+ ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath),
+ )
}
// getParentTreeFields returns list of parent tree names and corresponding tree paths
@@ -100,6 +104,7 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
}
func editFile(ctx *context.Context, isNewFile bool) {
+ ctx.Data["PageIsViewCode"] = true
ctx.Data["PageIsEdit"] = true
ctx.Data["IsNewFile"] = isNewFile
canCommit := renderCommitRights(ctx)
@@ -161,9 +166,6 @@ func editFile(ctx *context.Context, isNewFile bool) {
}
d, _ := io.ReadAll(dataRc)
- if err := dataRc.Close(); err != nil {
- log.Error("Error whilst closing blob data: %v", err)
- }
buf = append(buf, d...)
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
@@ -193,6 +195,9 @@ func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
+ ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
+ ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
+
ctx.HTML(http.StatusOK, tplEditFile)
}
@@ -262,9 +267,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
if isNewFile {
- message = ctx.Tr("repo.editor.add", form.TreePath)
+ message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
} else {
- message = ctx.Tr("repo.editor.update", form.TreePath)
+ message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
}
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@@ -336,15 +341,15 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplEditFile, &form)
+ ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form)
} else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplEditFile, &form)
+ ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form)
} else if git.IsErrPushRejected(err) {
errPushRej := err.(*git.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -356,7 +361,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ctx.RenderWithErr(flashError, tplEditFile, &form)
}
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
"Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
"Details": utils.SanitizeFlashErrorString(err.Error()),
@@ -415,7 +420,7 @@ func DiffPreviewPost(ctx *context.Context) {
}
if diff.NumFiles == 0 {
- ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show"))
+ ctx.PlainText(http.StatusOK, ctx.Locale.TrString("repo.editor.no_changes_to_show"))
return
}
ctx.Data["File"] = diff.Files[0]
@@ -482,7 +487,7 @@ func DeleteFilePost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
- message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
+ message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
if len(form.CommitMessage) > 0 {
@@ -545,7 +550,7 @@ func DeleteFilePost(ctx *context.Context) {
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
@@ -691,7 +696,7 @@ func UploadFilePost(ctx *context.Context) {
if dir == "" {
dir = "/"
}
- message = ctx.Tr("repo.editor.upload_files_to_dir", dir)
+ message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@@ -745,7 +750,7 @@ func UploadFilePost(ctx *context.Context) {
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.push_rejected"),
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(errPushRej.Message),
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index c28c3ef1d6..313fcfe33a 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -7,9 +7,9 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go
index daefe59c8f..9da4237c1e 100644
--- a/routers/web/repo/find.go
+++ b/routers/web/repo/find.go
@@ -7,7 +7,8 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -17,7 +18,7 @@ const (
// FindFiles render the page to find repository files
func FindFiles(ctx *context.Context) {
path := ctx.Params("*")
- ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + path
- ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + path
+ ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path)
+ ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path)
ctx.HTML(http.StatusOK, tplFindFiles)
}
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
new file mode 100644
index 0000000000..27e42a8f98
--- /dev/null
+++ b/routers/web/repo/fork.go
@@ -0,0 +1,236 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+ "net/http"
+ "net/url"
+
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplFork base.TplName = "repo/pulls/fork"
+)
+
+func getForkRepository(ctx *context.Context) *repo_model.Repository {
+ forkRepo := ctx.Repo.Repository
+ if ctx.Written() {
+ return nil
+ }
+
+ if forkRepo.IsEmpty {
+ log.Trace("Empty repository %-v", forkRepo)
+ ctx.NotFound("getForkRepository", nil)
+ return nil
+ }
+
+ if err := forkRepo.LoadOwner(ctx); err != nil {
+ ctx.ServerError("LoadOwner", err)
+ return nil
+ }
+
+ ctx.Data["repo_name"] = forkRepo.Name
+ ctx.Data["description"] = forkRepo.Description
+ ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
+ canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
+
+ ctx.Data["ForkRepo"] = forkRepo
+
+ ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
+ return nil
+ }
+ var orgs []*organization.Organization
+ for _, org := range ownedOrgs {
+ if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
+ orgs = append(orgs, org)
+ }
+ }
+
+ traverseParentRepo := forkRepo
+ for {
+ if ctx.Doer.ID == traverseParentRepo.OwnerID {
+ canForkToUser = false
+ } else {
+ for i, org := range orgs {
+ if org.ID == traverseParentRepo.OwnerID {
+ orgs = append(orgs[:i], orgs[i+1:]...)
+ break
+ }
+ }
+ }
+
+ if !traverseParentRepo.IsFork {
+ break
+ }
+ traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByID", err)
+ return nil
+ }
+ }
+
+ ctx.Data["CanForkToUser"] = canForkToUser
+ ctx.Data["Orgs"] = orgs
+
+ if canForkToUser {
+ ctx.Data["ContextUser"] = ctx.Doer
+ } else if len(orgs) > 0 {
+ ctx.Data["ContextUser"] = orgs[0]
+ } else {
+ ctx.Data["CanForkRepo"] = false
+ ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
+ return nil
+ }
+
+ branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
+ RepoID: ctx.Repo.Repository.ID,
+ ListOptions: db.ListOptionsAll,
+ IsDeletedBranch: optional.Some(false),
+ // Add it as the first option
+ ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
+ })
+ if err != nil {
+ ctx.ServerError("FindBranchNames", err)
+ return nil
+ }
+ ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
+
+ return forkRepo
+}
+
+// Fork render repository fork page
+func Fork(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("new_fork")
+
+ if ctx.Doer.CanForkRepo() {
+ ctx.Data["CanForkRepo"] = true
+ } else {
+ maxCreationLimit := ctx.Doer.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.Flash.Error(msg, true)
+ }
+
+ getForkRepository(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplFork)
+}
+
+// ForkPost response for forking a repository
+func ForkPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.CreateRepoForm)
+ ctx.Data["Title"] = ctx.Tr("new_fork")
+ ctx.Data["CanForkRepo"] = true
+
+ ctxUser := checkContextUser(ctx, form.UID)
+ if ctx.Written() {
+ return
+ }
+
+ forkRepo := getForkRepository(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["ContextUser"] = ctxUser
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplFork)
+ return
+ }
+
+ var err error
+ traverseParentRepo := forkRepo
+ for {
+ if ctxUser.ID == traverseParentRepo.OwnerID {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ return
+ }
+ repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
+ if repo != nil {
+ ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ return
+ }
+ if !traverseParentRepo.IsFork {
+ break
+ }
+ traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByID", err)
+ return
+ }
+ }
+
+ // Check if user is allowed to create repo's on the organization.
+ if ctxUser.IsOrganization() {
+ isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("CanCreateOrgRepo", err)
+ return
+ } else if !isAllowedToFork {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
+ BaseRepo: forkRepo,
+ Name: form.RepoName,
+ Description: form.Description,
+ SingleBranch: form.ForkSingleBranch,
+ })
+ if err != nil {
+ ctx.Data["Err_RepoName"] = true
+ switch {
+ case repo_model.IsErrReachLimitOfRepo(err):
+ maxCreationLimit := ctxUser.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.RenderWithErr(msg, tplFork, &form)
+ case repo_model.IsErrRepoAlreadyExist(err):
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ case repo_model.IsErrRepoFilesAlreadyExist(err):
+ switch {
+ case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
+ case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
+ case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
+ default:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
+ }
+ case db.IsErrNameReserved(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+ case errors.Is(err, user_model.ErrBlockedUser):
+ ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
+ default:
+ ctx.ServerError("ForkPost", err)
+ }
+ return
+ }
+
+ log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
+ ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index f52abbfb02..8fb6d93068 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -24,13 +24,13 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/go-chi/cors"
@@ -183,7 +183,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
if repoExist {
// Because of special ref "refs/for" .. , need delay write permission check
- if git.SupportProcReceive {
+ if git.DefaultFeatures.SupportProcReceive {
accessMode = perm.AccessModeRead
}
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index a98abe566f..5e1e116018 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -8,8 +8,8 @@ import (
"sort"
"code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/services/context"
)
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index c58cef8d64..6ff983f71a 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -9,6 +9,7 @@ import (
stdCtx "context"
"errors"
"fmt"
+ "html/template"
"math/big"
"net/http"
"net/url"
@@ -31,7 +32,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
@@ -39,21 +40,25 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
+ user_service "code.gitea.io/gitea/services/user"
)
const (
@@ -137,7 +142,7 @@ func MustAllowPulls(ctx *context.Context) {
}
}
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption util.OptionalBool) {
+func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
var err error
viewType := ctx.FormString("type")
sortType := ctx.FormString("sort")
@@ -183,8 +188,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
if len(selectLabels) > 0 {
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
if err != nil {
- ctx.ServerError("StringsToInt64s", err)
- return
+ ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
}
}
@@ -238,18 +242,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
}
- var isShowClosed util.OptionalBool
+ var isShowClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
- isShowClosed = util.OptionalBoolTrue
+ isShowClosed = optional.Some(true)
case "all":
- isShowClosed = util.OptionalBoolNone
+ isShowClosed = optional.None[bool]()
default:
- isShowClosed = util.OptionalBoolFalse
+ isShowClosed = optional.Some(false)
}
// if there are closed issues and no open issues, default to showing all issues
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
- isShowClosed = util.OptionalBoolNone
+ isShowClosed = optional.None[bool]()
}
if repo.IsTimetrackerEnabled(ctx) {
@@ -269,10 +273,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
var total int
- switch isShowClosed {
- case util.OptionalBoolTrue:
+ switch {
+ case isShowClosed.Value():
total = int(issueStats.ClosedCount)
- case util.OptionalBoolNone:
+ case !isShowClosed.Has():
total = int(issueStats.OpenCount + issueStats.ClosedCount)
default:
total = int(issueStats.OpenCount)
@@ -320,15 +324,15 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
return
}
- // Get posters.
- for i := range issues {
- // Check read status
- if !ctx.IsSigned {
- issues[i].IsRead = true
- } else if err = issues[i].GetIsRead(ctx, ctx.Doer.ID); err != nil {
- ctx.ServerError("GetIsRead", err)
+ if ctx.IsSigned {
+ if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil {
+ ctx.ServerError("LoadIsRead", err)
return
}
+ } else {
+ for i := range issues {
+ issues[i].IsRead = true
+ }
}
commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
@@ -428,7 +432,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
return
}
- pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.IsTrue())
+ pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value())
if err != nil {
ctx.ServerError("GetPinnedIssues", err)
return
@@ -442,13 +446,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
- mentionedID, projectID, assigneeID, posterID, archived)
+ milestoneID, projectID, assigneeID, posterID, archived)
ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
- mentionedID, projectID, assigneeID, posterID, archived)
+ milestoneID, projectID, assigneeID, posterID, archived)
ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels),
- mentionedID, projectID, assigneeID, posterID, archived)
+ milestoneID, projectID, assigneeID, posterID, archived)
ctx.Data["SelLabelIDs"] = labelIDs
ctx.Data["SelectLabels"] = selectLabels
ctx.Data["ViewType"] = viewType
@@ -458,26 +462,26 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ctx.Data["AssigneeID"] = assigneeID
ctx.Data["PosterID"] = posterID
ctx.Data["Keyword"] = keyword
- switch isShowClosed {
- case util.OptionalBoolTrue:
+ switch {
+ case isShowClosed.Value():
ctx.Data["State"] = "closed"
- case util.OptionalBoolNone:
+ case !isShowClosed.Has():
ctx.Data["State"] = "all"
default:
ctx.Data["State"] = "open"
}
ctx.Data["ShowArchivedLabels"] = archived
- pager.AddParam(ctx, "q", "Keyword")
- pager.AddParam(ctx, "type", "ViewType")
- pager.AddParam(ctx, "sort", "SortType")
- pager.AddParam(ctx, "state", "State")
- pager.AddParam(ctx, "labels", "SelectLabels")
- pager.AddParam(ctx, "milestone", "MilestoneID")
- pager.AddParam(ctx, "project", "ProjectID")
- pager.AddParam(ctx, "assignee", "AssigneeID")
- pager.AddParam(ctx, "poster", "PosterID")
- pager.AddParam(ctx, "archived", "ShowArchivedLabels")
+ pager.AddParamString("q", keyword)
+ pager.AddParamString("type", viewType)
+ pager.AddParamString("sort", sortType)
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+ pager.AddParamString("labels", fmt.Sprint(selectLabels))
+ pager.AddParamString("milestone", fmt.Sprint(milestoneID))
+ pager.AddParamString("project", fmt.Sprint(projectID))
+ pager.AddParamString("assignee", fmt.Sprint(assigneeID))
+ pager.AddParamString("poster", fmt.Sprint(posterID))
+ pager.AddParamString("archived", fmt.Sprint(archived))
ctx.Data["Page"] = pager
}
@@ -510,7 +514,7 @@ func Issues(ctx *context.Context) {
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
}
- issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), util.OptionalBoolOf(isPullList))
+ issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
if ctx.Written() {
return
}
@@ -552,7 +556,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
var err error
ctx.Data["OpenMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
@@ -560,7 +564,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
}
ctx.Data["ClosedMilestones"], err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
})
if err != nil {
ctx.ServerError("GetMilestones", err)
@@ -584,52 +588,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
if repo.Owner.IsOrganization() {
repoOwnerType = project_model.TypeOrganization
}
+
+ projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
+
+ var openProjects []*project_model.Project
+ var closedProjects []*project_model.Project
var err error
- projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
- ListOptions: db.ListOptionsAll,
- RepoID: repo.ID,
- IsClosed: util.OptionalBoolFalse,
- Type: project_model.TypeRepository,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
- }
- projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
- ListOptions: db.ListOptionsAll,
- OwnerID: repo.OwnerID,
- IsClosed: util.OptionalBoolFalse,
- Type: repoOwnerType,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
+
+ if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+ openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ RepoID: repo.ID,
+ IsClosed: optional.Some(false),
+ Type: project_model.TypeRepository,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+ closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ RepoID: repo.ID,
+ IsClosed: optional.Some(true),
+ Type: project_model.TypeRepository,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
}
- ctx.Data["OpenProjects"] = append(projects, projects2...)
-
- projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
- ListOptions: db.ListOptionsAll,
- RepoID: repo.ID,
- IsClosed: util.OptionalBoolTrue,
- Type: project_model.TypeRepository,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
- }
- projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
- ListOptions: db.ListOptionsAll,
- OwnerID: repo.OwnerID,
- IsClosed: util.OptionalBoolTrue,
- Type: repoOwnerType,
- })
- if err != nil {
- ctx.ServerError("GetProjects", err)
- return
+ if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
+ openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ OwnerID: repo.OwnerID,
+ IsClosed: optional.Some(false),
+ Type: repoOwnerType,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+ openProjects = append(openProjects, openProjects2...)
+ closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+ ListOptions: db.ListOptionsAll,
+ OwnerID: repo.OwnerID,
+ IsClosed: optional.Some(true),
+ Type: repoOwnerType,
+ })
+ if err != nil {
+ ctx.ServerError("GetProjects", err)
+ return
+ }
+ closedProjects = append(closedProjects, closedProjects2...)
}
- ctx.Data["ClosedProjects"] = append(projects, projects2...)
+ ctx.Data["OpenProjects"] = openProjects
+ ctx.Data["ClosedProjects"] = closedProjects
}
// repoReviewerSelection items to bee shown
@@ -712,16 +727,12 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
tmp.ItemID = -review.ReviewerTeamID
}
- if ctx.Repo.IsAdmin() {
- // Admin can dismiss or re-request any review requests
+ if canChooseReviewer {
+ // Users who can choose reviewers can also remove review requests
tmp.CanChange = true
} else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
// A user can refuse review requests
tmp.CanChange = true
- } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
- ctx.Doer.ID != review.ReviewerID {
- // The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
- tmp.CanChange = true
}
pullReviews = append(pullReviews, tmp)
@@ -994,17 +1005,17 @@ func NewIssue(ctx *context.Context) {
}
ctx.Data["Tags"] = tags
- _, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+ ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
for k, v := range errs {
- templateErrs[k] = v
+ ret.TemplateErrors[k] = v
}
if ctx.Written() {
return
}
- if len(templateErrs) > 0 {
- ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ if len(ret.TemplateErrors) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
}
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
@@ -1018,7 +1029,7 @@ func NewIssue(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplIssueNew)
}
-func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
+func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) template.HTML {
var files []string
for k := range errs {
files = append(files, k)
@@ -1030,14 +1041,14 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
}
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
"Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
"Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
})
if err != nil {
log.Debug("render flash error: %v", err)
- flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
+ flashError = ctx.Locale.Tr("repo.issues.choose.ignore_invalid_templates")
}
return flashError
}
@@ -1047,11 +1058,11 @@ func NewIssueChooseTemplate(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
- issueTemplates, errs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- ctx.Data["IssueTemplates"] = issueTemplates
+ ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+ ctx.Data["IssueTemplates"] = ret.IssueTemplates
- if len(errs) > 0 {
- ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
+ if len(ret.TemplateErrors) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, ret.TemplateErrors), true)
}
if !issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) {
@@ -1213,6 +1224,14 @@ func NewIssuePost(ctx *context.Context) {
return
}
+ if projectID > 0 {
+ if !ctx.Repo.CanRead(unit.TypeProjects) {
+ // User must also be able to see the project.
+ ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
+ return
+ }
+ }
+
if setting.Attachment.Enabled {
attachments = form.Files
}
@@ -1245,27 +1264,17 @@ func NewIssuePost(ctx *context.Context) {
Ref: form.Ref,
}
- if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
+ if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
+ } else {
+ ctx.ServerError("NewIssue", err)
}
- ctx.ServerError("NewIssue", err)
return
}
- if projectID > 0 {
- if !ctx.Repo.CanRead(unit.TypeProjects) {
- // User must also be able to see the project.
- ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
- return
- }
- if err := issues_model.ChangeProjectAssign(ctx, issue, ctx.Doer, projectID); err != nil {
- ctx.ServerError("ChangeProjectAssign", err)
- return
- }
- }
-
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
@@ -1440,7 +1449,7 @@ func ViewIssue(ctx *context.Context) {
return
}
- ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+ ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
iw := new(issues_model.IssueWatch)
if ctx.Doer != nil {
@@ -1526,18 +1535,9 @@ func ViewIssue(ctx *context.Context) {
}
if issue.IsPull {
- canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
+ canChooseReviewer := false
if ctx.Doer != nil && ctx.IsSigned {
- if !canChooseReviewer {
- canChooseReviewer = ctx.Doer.ID == issue.PosterID
- }
- if !canChooseReviewer {
- canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
- if err != nil {
- ctx.ServerError("IsOfficialReviewer", err)
- return
- }
- }
+ canChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, issue)
}
RetrieveRepoReviewers(ctx, repo, issue, canChooseReviewer)
@@ -1602,22 +1602,22 @@ func ViewIssue(ctx *context.Context) {
}
marked[issue.PosterID] = issue.ShowRole
- // Render comments and and fetch participants.
+ // Render comments and fetch participants.
participants[0] = issue.Poster
+
+ if err := issue.Comments.LoadAttachmentsByIssue(ctx); err != nil {
+ ctx.ServerError("LoadAttachmentsByIssue", err)
+ return
+ }
+ if err := issue.Comments.LoadPosters(ctx); err != nil {
+ ctx.ServerError("LoadPosters", err)
+ return
+ }
+
for _, comment = range issue.Comments {
comment.Issue = issue
- if err := comment.LoadPoster(ctx); err != nil {
- ctx.ServerError("LoadPoster", err)
- return
- }
-
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
- if err := comment.LoadAttachments(ctx); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
-
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
@@ -1656,7 +1656,7 @@ func ViewIssue(ctx *context.Context) {
}
ghostMilestone := &issues_model.Milestone{
ID: -1,
- Name: ctx.Tr("repo.issues.deleted_milestone"),
+ Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
}
if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
comment.OldMilestone = ghostMilestone
@@ -1665,7 +1665,6 @@ func ViewIssue(ctx *context.Context) {
comment.Milestone = ghostMilestone
}
} else if comment.Type == issues_model.CommentTypeProject {
-
if err = comment.LoadProject(ctx); err != nil {
ctx.ServerError("LoadProject", err)
return
@@ -1673,7 +1672,7 @@ func ViewIssue(ctx *context.Context) {
ghostProject := &project_model.Project{
ID: -1,
- Title: ctx.Tr("repo.issues.deleted_project"),
+ Title: ctx.Locale.TrString("repo.issues.deleted_project"),
}
if comment.OldProjectID > 0 && comment.OldProject == nil {
@@ -1768,7 +1767,7 @@ func ViewIssue(ctx *context.Context) {
// so "|" is used as delimeter to mark the new format
if comment.Content[0] != '|' {
// handle old time comments that have formatted text stored
- comment.RenderedContent = comment.Content
+ comment.RenderedContent = templates.SanitizeHTML(comment.Content)
comment.Content = ""
} else {
// else it's just a duration in seconds to pass on to the frontend
@@ -1863,6 +1862,8 @@ func ViewIssue(ctx *context.Context) {
mergeStyle = repo_model.MergeStyleRebaseMerge
} else if prConfig.AllowSquash {
mergeStyle = repo_model.MergeStyleSquash
+ } else if prConfig.AllowFastForwardOnly {
+ mergeStyle = repo_model.MergeStyleFastForwardOnly
} else if prConfig.AllowManualMerge {
mergeStyle = repo_model.MergeStyleManuallyMerged
}
@@ -2040,6 +2041,10 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["Tags"] = tags
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+
ctx.HTML(http.StatusOK, tplIssueView)
}
@@ -2175,7 +2180,7 @@ func GetIssueInfo(ctx *context.Context) {
}
}
- ctx.JSON(http.StatusOK, convert.ToIssue(ctx, issue))
+ ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue))
}
// UpdateIssueTitle change issue's title
@@ -2243,7 +2248,11 @@ func UpdateIssueContent(ctx *context.Context) {
}
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, ctx.Req.FormValue("content")); err != nil {
- ctx.ServerError("ChangeContent", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.edit.blocked_user"))
+ } else {
+ ctx.ServerError("ChangeContent", err)
+ }
return
}
@@ -2490,6 +2499,10 @@ func UpdatePullReviewRequest(ctx *context.Context) {
_, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach")
if err != nil {
+ if issues_model.IsErrReviewRequestOnClosedPR(err) {
+ ctx.Status(http.StatusForbidden)
+ return
+ }
ctx.ServerError("ReviewRequest", err)
return
}
@@ -2506,14 +2519,14 @@ func SearchIssues(ctx *context.Context) {
return
}
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
- isClosed = util.OptionalBoolTrue
+ isClosed = optional.Some(true)
case "all":
- isClosed = util.OptionalBoolNone
+ isClosed = optional.None[bool]()
default:
- isClosed = util.OptionalBoolFalse
+ isClosed = optional.Some(false)
}
var (
@@ -2526,7 +2539,7 @@ func SearchIssues(ctx *context.Context) {
Private: false,
AllPublic: true,
TopicOnly: false,
- Collaborate: util.OptionalBoolNone,
+ Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
@@ -2549,7 +2562,7 @@ func SearchIssues(ctx *context.Context) {
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
- opts.Collaborate = util.OptionalBoolFalse
+ opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
@@ -2588,14 +2601,12 @@ func SearchIssues(ctx *context.Context) {
keyword = ""
}
- var isPull util.OptionalBool
+ isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
case "issues":
- isPull = util.OptionalBoolFalse
- default:
- isPull = util.OptionalBoolNone
+ isPull = optional.Some(false)
}
var includedAnyLabels []int64
@@ -2627,9 +2638,9 @@ func SearchIssues(ctx *context.Context) {
}
}
- var projectID *int64
+ projectID := optional.None[int64]()
if v := ctx.FormInt64("project"); v > 0 {
- projectID = &v
+ projectID = optional.Some(v)
}
// this api is also used in UI,
@@ -2658,28 +2669,28 @@ func SearchIssues(ctx *context.Context) {
}
if since != 0 {
- searchOpt.UpdatedAfterUnix = &since
+ searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
- searchOpt.UpdatedBeforeUnix = &before
+ searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
- searchOpt.PosterID = &ctxUserID
+ searchOpt.PosterID = optional.Some(ctxUserID)
}
if ctx.FormBool("assigned") {
- searchOpt.AssigneeID = &ctxUserID
+ searchOpt.AssigneeID = optional.Some(ctxUserID)
}
if ctx.FormBool("mentioned") {
- searchOpt.MentionID = &ctxUserID
+ searchOpt.MentionID = optional.Some(ctxUserID)
}
if ctx.FormBool("review_requested") {
- searchOpt.ReviewRequestedID = &ctxUserID
+ searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
}
if ctx.FormBool("reviewed") {
- searchOpt.ReviewedID = &ctxUserID
+ searchOpt.ReviewedID = optional.Some(ctxUserID)
}
}
@@ -2699,7 +2710,7 @@ func SearchIssues(ctx *context.Context) {
}
ctx.SetTotalCountHeader(total)
- ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
+ ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
}
func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
@@ -2730,14 +2741,14 @@ func ListIssues(ctx *context.Context) {
return
}
- var isClosed util.OptionalBool
+ var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
- isClosed = util.OptionalBoolTrue
+ isClosed = optional.Some(true)
case "all":
- isClosed = util.OptionalBoolNone
+ isClosed = optional.None[bool]()
default:
- isClosed = util.OptionalBoolFalse
+ isClosed = optional.Some(false)
}
keyword := ctx.FormTrim("q")
@@ -2784,19 +2795,17 @@ func ListIssues(ctx *context.Context) {
}
}
- var projectID *int64
+ projectID := optional.None[int64]()
if v := ctx.FormInt64("project"); v > 0 {
- projectID = &v
+ projectID = optional.Some(v)
}
- var isPull util.OptionalBool
+ isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
- isPull = util.OptionalBoolTrue
+ isPull = optional.Some(true)
case "issues":
- isPull = util.OptionalBoolFalse
- default:
- isPull = util.OptionalBoolNone
+ isPull = optional.Some(false)
}
// FIXME: we should be more efficient here
@@ -2826,10 +2835,10 @@ func ListIssues(ctx *context.Context) {
SortBy: issue_indexer.SortByCreatedDesc,
}
if since != 0 {
- searchOpt.UpdatedAfterUnix = &since
+ searchOpt.UpdatedAfterUnix = optional.Some(since)
}
if before != 0 {
- searchOpt.UpdatedBeforeUnix = &before
+ searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
if len(labelIDs) == 1 && labelIDs[0] == 0 {
searchOpt.NoLabelOnly = true
@@ -2850,13 +2859,13 @@ func ListIssues(ctx *context.Context) {
}
if createdByID > 0 {
- searchOpt.PosterID = &createdByID
+ searchOpt.PosterID = optional.Some(createdByID)
}
if assignedByID > 0 {
- searchOpt.AssigneeID = &assignedByID
+ searchOpt.AssigneeID = optional.Some(assignedByID)
}
if mentionedByID > 0 {
- searchOpt.MentionID = &mentionedByID
+ searchOpt.MentionID = optional.Some(mentionedByID)
}
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
@@ -2871,7 +2880,7 @@ func ListIssues(ctx *context.Context) {
}
ctx.SetTotalCountHeader(total)
- ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
+ ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
}
func BatchDeleteIssues(ctx *context.Context) {
@@ -3105,7 +3114,11 @@ func NewComment(ctx *context.Context) {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
- ctx.ServerError("CreateIssueComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else {
+ ctx.ServerError("CreateIssueComment", err)
+ }
return
}
@@ -3149,7 +3162,11 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
- ctx.ServerError("UpdateComment", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
return
}
@@ -3257,9 +3274,9 @@ func ChangeIssueReaction(ctx *context.Context) {
switch ctx.Params(":action") {
case "react":
- reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Content)
+ reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Content)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err)
return
}
@@ -3301,7 +3318,7 @@ func ChangeIssueReaction(ctx *context.Context) {
return
}
- html, err := ctx.RenderToString(tplReactions, map[string]any{
+ html, err := ctx.RenderToHTML(tplReactions, map[string]any{
"ctxData": ctx.Data,
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
"Reactions": issue.Reactions.GroupByType(),
@@ -3364,9 +3381,9 @@ func ChangeCommentReaction(ctx *context.Context) {
switch ctx.Params(":action") {
case "react":
- reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Content)
+ reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Content)
if err != nil {
- if issues_model.IsErrForbiddenIssueReaction(err) {
+ if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.ServerError("ChangeIssueReaction", err)
return
}
@@ -3408,7 +3425,7 @@ func ChangeCommentReaction(ctx *context.Context) {
return
}
- html, err := ctx.RenderToString(tplReactions, map[string]any{
+ html, err := ctx.RenderToHTML(tplReactions, map[string]any{
"ctxData": ctx.Data,
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
"Reactions": comment.Reactions.GroupByType(),
@@ -3551,8 +3568,8 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
return err
}
-func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) string {
- attachHTML, err := ctx.RenderToString(tplAttachment, map[string]any{
+func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
+ attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
"ctxData": ctx.Data,
"Attachments": attachments,
"Content": content,
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 0f376db145..bf3571c835 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -11,11 +11,11 @@ import (
"code.gitea.io/gitea/models/avatars"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/services/context"
"github.com/sergi/go-diff/diffmatchpatch"
)
@@ -56,12 +56,12 @@ func GetContentHistoryList(ctx *context.Context) {
for _, item := range items {
var actionText string
if item.IsDeleted {
- actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
+ actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
actionText = "" + actionTextDeleted + ""
} else if item.IsFirstCreated {
- actionText = ctx.Locale.Tr("repo.issues.content_history.created")
+ actionText = ctx.Locale.TrString("repo.issues.content_history.created")
} else {
- actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
+ actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
}
username := item.UserName
@@ -70,7 +70,7 @@ func GetContentHistoryList(ctx *context.Context) {
}
src := html.EscapeString(item.UserAvatarLink)
- class := avatars.DefaultAvatarClass + " gt-mr-3"
+ class := avatars.DefaultAvatarClass + " tw-mr-2"
name := html.EscapeString(username)
avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))
@@ -94,7 +94,7 @@ func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue
// CanWrite means the doer can manage the issue/PR list
if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
canSoftDelete = true
- } else {
+ } else if ctx.Doer != nil {
// for read-only users, they could still post issues or comments,
// they should be able to delete the history related to their own issue/comment, a case is:
// 1. the user posts some sensitive data
@@ -186,6 +186,10 @@ func SoftDeleteContentHistory(ctx *context.Context) {
if ctx.Written() {
return
}
+ if ctx.Doer == nil {
+ ctx.NotFound("Require SignIn", nil)
+ return
+ }
commentID := ctx.FormInt64("comment_id")
historyID := ctx.FormInt64("history_id")
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index 022ec3ae3e..e3b85ee638 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -8,8 +8,8 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
// AddDependency adds new dependencies
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index dd3e2803b4..81bee4dbb5 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -10,12 +10,11 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -112,12 +111,11 @@ func NewLabel(ctx *context.Context) {
}
l := &issues_model.Label{
- RepoID: ctx.Repo.Repository.ID,
- Name: form.Title,
- Exclusive: form.Exclusive,
- Description: form.Description,
- Color: form.Color,
- ArchivedUnix: timeutil.TimeStamp(0),
+ RepoID: ctx.Repo.Repository.ID,
+ Name: form.Title,
+ Exclusive: form.Exclusive,
+ Description: form.Description,
+ Color: form.Color,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index e0d49e44e1..93fc72300b 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -10,10 +10,10 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
@@ -123,7 +123,7 @@ func TestDeleteLabel(t *testing.T) {
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
- assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
+ assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
func TestUpdateIssueLabel_Clear(t *testing.T) {
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index f83109d9b3..1d5fc8a5f3 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -5,8 +5,8 @@ package repo
import (
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go
index 9f334129f9..365c812681 100644
--- a/routers/web/repo/issue_pin.go
+++ b/routers/web/repo/issue_pin.go
@@ -7,9 +7,9 @@ import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
// IssuePinOrUnpin pin or unpin a Issue
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index ab9fe3e69d..70d42b27c0 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -9,8 +9,8 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
+ "code.gitea.io/gitea/services/context"
)
// IssueStopwatch creates or stops a stopwatch for the given issue.
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index c9bf861b84..241e434049 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -9,9 +9,9 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 1f51ceba5e..8b033f3b17 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -9,8 +9,8 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
index d70a53030e..420931c5fb 100644
--- a/routers/web/repo/middlewares.go
+++ b/routers/web/repo/middlewares.go
@@ -9,9 +9,9 @@ import (
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index b70901d5f2..97b0c425ea 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -15,13 +15,13 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
"code.gitea.io/gitea/services/task"
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 19db2abd68..95a4fe60cc 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -12,13 +12,13 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/issue"
@@ -51,7 +51,7 @@ func Milestones(ctx *context.Context) {
PageSize: setting.UI.IssuePagingNum,
},
RepoID: ctx.Repo.Repository.ID,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: optional.Some(isShowClosed),
SortType: sortType,
Name: keyword,
})
@@ -106,8 +106,8 @@ func Milestones(ctx *context.Context) {
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
- pager.AddParam(ctx, "state", "State")
- pager.AddParam(ctx, "q", "Keyword")
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+ pager.AddParamString("q", keyword)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestone)
@@ -292,10 +292,10 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
ctx.Data["Title"] = milestone.Name
ctx.Data["Milestone"] = milestone
- issues(ctx, milestoneID, projectID, util.OptionalBoolNone)
+ issues(ctx, milestoneID, projectID, optional.None[bool]())
- ret, _ := issue.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- ctx.Data["NewIssueChooseTemplate"] = len(ret) > 0
+ ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
+ ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true)
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index ac9e64d774..57e578da37 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -10,9 +10,9 @@ import (
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -37,7 +37,7 @@ func Packages(ctx *context.Context) {
RepoID: ctx.Repo.Repository.ID,
Type: packages.Type(packageType),
Name: packages.SearchValue{Value: query},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
ctx.ServerError("SearchLatestVersions", err)
@@ -70,8 +70,8 @@ func Packages(ctx *context.Context) {
ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
- pager.AddParam(ctx, "q", "Query")
- pager.AddParam(ctx, "type", "PackageType")
+ pager.AddParamString("q", query)
+ pager.AddParamString("type", packageType)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplPackagesList)
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
index c04435cf1b..0dee02dd9c 100644
--- a/routers/web/repo/patch.go
+++ b/routers/web/repo/patch.go
@@ -10,10 +10,10 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/repository/files"
)
@@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) {
// `message` will be both the summary and message combined
message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 {
- message = ctx.Tr("repo.editor.patch")
+ message = ctx.Locale.TrString("repo.editor.patch")
}
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 4908bb796d..a2db1fc770 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -14,16 +14,16 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
project_model "code.gitea.io/gitea/models/project"
- attachment_model "code.gitea.io/gitea/models/repo"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -33,16 +33,17 @@ const (
tplProjectsView base.TplName = "repo/projects/view"
)
-// MustEnableProjects check if projects are enabled in settings
-func MustEnableProjects(ctx *context.Context) {
+// MustEnableRepoProjects check if repo projects are enabled in settings
+func MustEnableRepoProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() {
ctx.NotFound("EnableKanbanBoard", nil)
return
}
if ctx.Repo.Repository != nil {
- if !ctx.Repo.CanRead(unit.TypeProjects) {
- ctx.NotFound("MustEnableProjects", nil)
+ projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
+ if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+ ctx.NotFound("MustEnableRepoProjects", nil)
return
}
}
@@ -78,7 +79,7 @@ func Projects(ctx *context.Context) {
Page: page,
},
RepoID: repo.ID,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: optional.Some(isShowClosed),
OrderBy: project_model.GetSearchOrderByBySortType(sortType),
Type: project_model.TypeRepository,
Title: keyword,
@@ -117,7 +118,7 @@ func Projects(ctx *context.Context) {
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
- pager.AddParam(ctx, "state", "State")
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
@@ -314,10 +315,6 @@ func ViewProject(ctx *context.Context) {
return
}
- if boards[0].ID == 0 {
- boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
- }
-
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err)
@@ -325,10 +322,10 @@ func ViewProject(ctx *context.Context) {
}
if project.CardType != project_model.CardTypeTextOnly {
- issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
+ issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
- if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
+ if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
issuesAttachmentMap[issue.ID] = issueAttachment
}
}
@@ -339,17 +336,17 @@ func ViewProject(ctx *context.Context) {
linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
- var referencedIds []int64
+ var referencedIDs []int64
for _, comment := range issue.Comments {
if comment.RefIssueID != 0 && comment.RefIsPull {
- referencedIds = append(referencedIds, comment.RefIssueID)
+ referencedIDs = append(referencedIDs, comment.RefIssueID)
}
}
- if len(referencedIds) > 0 {
+ if len(referencedIDs) > 0 {
if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
- IssueIDs: referencedIds,
- IsPull: util.OptionalBoolTrue,
+ IssueIDs: referencedIDs,
+ IsPull: optional.Some(true),
}); err == nil {
linkedPrsMap[issue.ID] = linkedPrs
}
@@ -582,21 +579,6 @@ func SetDefaultProjectBoard(ctx *context.Context) {
ctx.JSONOK()
}
-// UnSetDefaultProjectBoard unset default board for uncategorized issues/pulls
-func UnSetDefaultProjectBoard(ctx *context.Context) {
- project, _ := checkProjectBoardChangePermissions(ctx)
- if ctx.Written() {
- return
- }
-
- if err := project_model.SetDefaultBoard(ctx, project.ID, 0); err != nil {
- ctx.ServerError("SetDefaultBoard", err)
- return
- }
-
- ctx.JSONOK()
-}
-
// MoveIssues moves or keeps issues in a column and sorts them inside that column
func MoveIssues(ctx *context.Context) {
if ctx.Doer == nil {
@@ -627,28 +609,19 @@ func MoveIssues(ctx *context.Context) {
return
}
- var board *project_model.Board
+ board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
+ if err != nil {
+ if project_model.IsErrProjectBoardNotExist(err) {
+ ctx.NotFound("ProjectBoardNotExist", nil)
+ } else {
+ ctx.ServerError("GetProjectBoard", err)
+ }
+ return
+ }
- if ctx.ParamsInt64(":boardID") == 0 {
- board = &project_model.Board{
- ID: 0,
- ProjectID: project.ID,
- Title: ctx.Tr("repo.projects.type.uncategorized"),
- }
- } else {
- board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
- if err != nil {
- if project_model.IsErrProjectBoardNotExist(err) {
- ctx.NotFound("ProjectBoardNotExist", nil)
- } else {
- ctx.ServerError("GetProjectBoard", err)
- }
- return
- }
- if board.ProjectID != project.ID {
- ctx.NotFound("BoardNotInProject", nil)
- return
- }
+ if board.ProjectID != project.ID {
+ ctx.NotFound("BoardNotInProject", nil)
+ return
}
type movedIssuesForm struct {
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
index 6698d47028..479f8c55a2 100644
--- a/routers/web/repo/projects_test.go
+++ b/routers/web/repo/projects_test.go
@@ -7,7 +7,7 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 8b09ae5463..9b45fa23d5 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -10,7 +10,6 @@ import (
"fmt"
"html"
"net/http"
- "net/url"
"strconv"
"strings"
"time"
@@ -20,37 +19,36 @@ import (
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
+ user_service "code.gitea.io/gitea/services/user"
"github.com/gobwas/glob"
)
const (
- tplFork base.TplName = "repo/pulls/fork"
tplCompareDiff base.TplName = "repo/diff/compare"
tplPullCommits base.TplName = "repo/pulls/commits"
tplPullFiles base.TplName = "repo/pulls/files"
@@ -109,213 +107,6 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
return repo
}
-func getForkRepository(ctx *context.Context) *repo_model.Repository {
- forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
- if ctx.Written() {
- return nil
- }
-
- if forkRepo.IsEmpty {
- log.Trace("Empty repository %-v", forkRepo)
- ctx.NotFound("getForkRepository", nil)
- return nil
- }
-
- if err := forkRepo.LoadOwner(ctx); err != nil {
- ctx.ServerError("LoadOwner", err)
- return nil
- }
-
- ctx.Data["repo_name"] = forkRepo.Name
- ctx.Data["description"] = forkRepo.Description
- ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
- canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
-
- ctx.Data["ForkRepo"] = forkRepo
-
- ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
- return nil
- }
- var orgs []*organization.Organization
- for _, org := range ownedOrgs {
- if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
- orgs = append(orgs, org)
- }
- }
-
- traverseParentRepo := forkRepo
- for {
- if ctx.Doer.ID == traverseParentRepo.OwnerID {
- canForkToUser = false
- } else {
- for i, org := range orgs {
- if org.ID == traverseParentRepo.OwnerID {
- orgs = append(orgs[:i], orgs[i+1:]...)
- break
- }
- }
- }
-
- if !traverseParentRepo.IsFork {
- break
- }
- traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
- if err != nil {
- ctx.ServerError("GetRepositoryByID", err)
- return nil
- }
- }
-
- ctx.Data["CanForkToUser"] = canForkToUser
- ctx.Data["Orgs"] = orgs
-
- if canForkToUser {
- ctx.Data["ContextUser"] = ctx.Doer
- } else if len(orgs) > 0 {
- ctx.Data["ContextUser"] = orgs[0]
- } else {
- ctx.Data["CanForkRepo"] = false
- ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
- return nil
- }
-
- branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
- RepoID: ctx.Repo.Repository.ID,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
- IsDeletedBranch: util.OptionalBoolFalse,
- // Add it as the first option
- ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
- })
- if err != nil {
- ctx.ServerError("FindBranchNames", err)
- return nil
- }
- ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
-
- return forkRepo
-}
-
-// Fork render repository fork page
-func Fork(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("new_fork")
-
- if ctx.Doer.CanForkRepo() {
- ctx.Data["CanForkRepo"] = true
- } else {
- maxCreationLimit := ctx.Doer.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.Flash.Error(msg, true)
- }
-
- getForkRepository(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.HTML(http.StatusOK, tplFork)
-}
-
-// ForkPost response for forking a repository
-func ForkPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateRepoForm)
- ctx.Data["Title"] = ctx.Tr("new_fork")
- ctx.Data["CanForkRepo"] = true
-
- ctxUser := checkContextUser(ctx, form.UID)
- if ctx.Written() {
- return
- }
-
- forkRepo := getForkRepository(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.Data["ContextUser"] = ctxUser
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplFork)
- return
- }
-
- var err error
- traverseParentRepo := forkRepo
- for {
- if ctxUser.ID == traverseParentRepo.OwnerID {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
- return
- }
- repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
- if repo != nil {
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
- return
- }
- if !traverseParentRepo.IsFork {
- break
- }
- traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
- if err != nil {
- ctx.ServerError("GetRepositoryByID", err)
- return
- }
- }
-
- // Check if user is allowed to create repo's on the organization.
- if ctxUser.IsOrganization() {
- isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("CanCreateOrgRepo", err)
- return
- } else if !isAllowedToFork {
- ctx.Error(http.StatusForbidden)
- return
- }
- }
-
- repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
- BaseRepo: forkRepo,
- Name: form.RepoName,
- Description: form.Description,
- SingleBranch: form.ForkSingleBranch,
- })
- if err != nil {
- ctx.Data["Err_RepoName"] = true
- switch {
- case repo_model.IsErrReachLimitOfRepo(err):
- maxCreationLimit := ctxUser.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.RenderWithErr(msg, tplFork, &form)
- case repo_model.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
- case repo_model.IsErrRepoFilesAlreadyExist(err):
- switch {
- case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
- case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
- case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
- default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
- }
- case db.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
- case db.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
- default:
- ctx.ServerError("ForkPost", err)
- }
- return
- }
-
- log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
-}
-
func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
@@ -334,7 +125,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
ctx.ServerError("LoadRepo", err)
return nil, false
}
- ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
+ ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
ctx.Data["Issue"] = issue
if !issue.IsPull {
@@ -487,7 +278,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
if len(compareInfo.Commits) != 0 {
sha := compareInfo.Commits[0].ID.String()
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptions{ListAll: true})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -549,7 +340,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -641,7 +432,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -652,6 +443,24 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
}
if pb != nil && pb.EnableStatusCheck {
+
+ var missingRequiredChecks []string
+ for _, requiredContext := range pb.StatusCheckContexts {
+ contextFound := false
+ matchesRequiredContext := createRequiredContextMatcher(requiredContext)
+ for _, presentStatus := range commitStatuses {
+ if matchesRequiredContext(presentStatus.Context) {
+ contextFound = true
+ break
+ }
+ }
+
+ if !contextFound {
+ missingRequiredChecks = append(missingRequiredChecks, requiredContext)
+ }
+ }
+ ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
+
ctx.Data["is_context_required"] = func(context string) bool {
for _, c := range pb.StatusCheckContexts {
if c == context {
@@ -720,10 +529,22 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
return compareInfo
}
+func createRequiredContextMatcher(requiredContext string) func(string) bool {
+ if gp, err := glob.Compile(requiredContext); err == nil {
+ return func(contextToCheck string) bool {
+ return gp.Match(contextToCheck)
+ }
+ }
+
+ return func(contextToCheck string) bool {
+ return requiredContext == contextToCheck
+ }
+}
+
type pullCommitList struct {
Commits []pull_service.CommitInfo `json:"commits"`
LastReviewCommitSha string `json:"last_review_commit_sha"`
- Locale map[string]string `json:"locale"`
+ Locale map[string]any `json:"locale"`
}
// GetPullCommits get all commits for given pull request
@@ -741,7 +562,7 @@ func GetPullCommits(ctx *context.Context) {
}
// Get the needed locale
- resp.Locale = map[string]string{
+ resp.Locale = map[string]any{
"lang": ctx.Locale.Language(),
"show_all_commits": ctx.Tr("repo.pulls.show_all_commits"),
"stats_num_commits": ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)),
@@ -938,6 +759,19 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
return
}
+ for _, file := range diff.Files {
+ for _, section := range file.Sections {
+ for _, line := range section.Lines {
+ for _, comment := range line.Comments {
+ if err := comment.LoadAttachments(ctx); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
+ }
+ }
+ }
+ }
+
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
if err != nil {
ctx.ServerError("LoadProtectedBranch", err)
@@ -1020,6 +854,36 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
upload.AddUploadContext(ctx, "comment")
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+ if !willShowSpecifiedCommit && !willShowSpecifiedCommitRange && pull.Flow == issues_model.PullRequestFlowGithub {
+ if err := pull.LoadHeadRepo(ctx); err != nil {
+ ctx.ServerError("LoadHeadRepo", err)
+ return
+ }
+
+ if pull.HeadRepo != nil {
+ ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pull.HeadBranch)
+ }
+
+ if !pull.HasMerged && ctx.Doer != nil {
+ perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
+
+ if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) {
+ ctx.Data["CanEditFile"] = true
+ ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
+ ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link()
+ ctx.Data["HeadBranchName"] = pull.HeadBranch
+ ctx.Data["BackToLink"] = setting.AppSubURL + ctx.Req.URL.RequestURI()
+ }
+ }
+ }
+
ctx.HTML(http.StatusOK, tplPullFiles)
}
@@ -1084,7 +948,7 @@ func UpdatePullRequest(ctx *context.Context) {
if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.merge_conflict"),
"Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1098,7 +962,7 @@ func UpdatePullRequest(ctx *context.Context) {
return
} else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1230,7 +1094,7 @@ func MergePullRequest(ctx *context.Context) {
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
} else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.editor.merge_conflict"),
"Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1243,7 +1107,7 @@ func MergePullRequest(ctx *context.Context) {
ctx.JSONRedirect(issue.Link())
} else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts)
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut),
@@ -1253,19 +1117,19 @@ func MergePullRequest(ctx *context.Context) {
return
}
ctx.Flash.Error(flashError)
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if models.IsErrMergeUnrelatedHistories(err) {
log.Debug("MergeUnrelatedHistories error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if git.IsErrPushOutOfDate(err) {
log.Debug("MergePushOutOfDate error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if models.IsErrSHADoesNotMatch(err) {
log.Debug("MergeHeadOutOfDate error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date"))
- ctx.Redirect(issue.Link())
+ ctx.JSONRedirect(issue.Link())
} else if git.IsErrPushRejected(err) {
log.Debug("MergePushRejected error: %v", err)
pushrejErr := err.(*git.ErrPushRejected)
@@ -1273,7 +1137,7 @@ func MergePullRequest(ctx *context.Context) {
if len(message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
} else {
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1438,7 +1302,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return
} else if git.IsErrPushRejected(err) {
pushrejErr := err.(*git.ErrPushRejected)
message := pushrejErr.Message
@@ -1446,7 +1309,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message"))
return
}
- flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
"Message": ctx.Tr("repo.pulls.push_rejected"),
"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
@@ -1455,11 +1318,18 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.ServerError("CompareAndPullRequest.HTMLString", err)
return
}
- ctx.Flash.Error(flashError)
- ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost
- return
+ ctx.JSONError(flashError)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+ "Message": ctx.Tr("repo.pulls.push_rejected"),
+ "Summary": ctx.Tr("repo.pulls.new.blocked_user"),
+ })
+ if err != nil {
+ ctx.ServerError("CompareAndPullRequest.HTMLString", err)
+ return
+ }
+ ctx.JSONError(flashError)
}
- ctx.ServerError("NewPullRequest", err)
return
}
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index b93460d169..c8d149a482 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -10,14 +10,17 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
pull_model "code.gitea.io/gitea/models/pull"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
+ user_service "code.gitea.io/gitea/services/user"
)
const (
@@ -50,6 +53,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
ctx.HTML(http.StatusOK, tplNewComment)
}
@@ -75,6 +80,11 @@ func CreateCodeComment(ctx *context.Context) {
signedLine *= -1
}
+ var attachments []string
+ if setting.Attachment.Enabled {
+ attachments = form.Files
+ }
+
comment, err := pull_service.CreateCodeComment(ctx,
ctx.Doer,
ctx.Repo.GitRepo,
@@ -85,6 +95,7 @@ func CreateCodeComment(ctx *context.Context) {
!form.SingleReview,
form.Reply,
form.LatestCommitID,
+ attachments,
)
if err != nil {
ctx.ServerError("CreateCodeComment", err)
@@ -156,7 +167,8 @@ func UpdateResolveConversation(ctx *context.Context) {
func renderConversation(ctx *context.Context, comment *issues_model.Comment, origin string) {
ctx.Data["PageIsPullFiles"] = origin == "diff"
- comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, ctx.Data["ShowOutdatedComments"].(bool))
+ showOutdatedComments := origin == "timeline" || ctx.Data["ShowOutdatedComments"].(bool)
+ comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line, showOutdatedComments)
if err != nil {
ctx.ServerError("FetchCodeCommentsByLine", err)
return
@@ -167,6 +179,14 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return
}
+ if err := comments.LoadAttachments(ctx); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
+
+ ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ upload.AddUploadContext(ctx, "comment")
+
ctx.Data["comments"] = comments
if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err)
@@ -183,6 +203,10 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
+ ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+ return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
+ }
+
if origin == "diff" {
ctx.HTML(http.StatusOK, tplDiffConversation)
} else if origin == "timeline" {
@@ -219,9 +243,9 @@ func SubmitReview(ctx *context.Context) {
if issue.IsPoster(ctx.Doer.ID) {
var translated string
if reviewType == issues_model.ReviewTypeApprove {
- translated = ctx.Tr("repo.issues.review.self.approval")
+ translated = ctx.Locale.TrString("repo.issues.review.self.approval")
} else {
- translated = ctx.Tr("repo.issues.review.self.rejection")
+ translated = ctx.Locale.TrString("repo.issues.review.self.rejection")
}
ctx.Flash.Error(translated)
@@ -253,6 +277,10 @@ func DismissReview(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.DismissReviewForm)
comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
if err != nil {
+ if pull_service.IsErrDismissRequestOnClosedPR(err) {
+ ctx.Status(http.StatusForbidden)
+ return
+ }
ctx.ServerError("pull_service.DismissReview", err)
return
}
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
index 65019af40b..8344ff4091 100644
--- a/routers/web/repo/pull_review_test.go
+++ b/routers/web/repo/pull_review_test.go
@@ -4,15 +4,16 @@
package repo
import (
+ "net/http"
"net/http/httptest"
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/pull"
"github.com/stretchr/testify/assert"
@@ -39,7 +40,7 @@ func TestRenderConversation(t *testing.T) {
var preparedComment *issues_model.Comment
run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
- comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID)
+ comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID, nil)
if !assert.NoError(t, err) {
return
}
@@ -68,9 +69,25 @@ func TestRenderConversation(t *testing.T) {
renderConversation(ctx, preparedComment, "timeline")
assert.Contains(t, resp.Body.String(), ` 0 {
- opts := repo_module.GenerateRepoOptions{
+ opts := repo_service.GenerateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Private: form.Private,
@@ -312,13 +314,13 @@ func Action(ctx *context.Context) {
var err error
switch ctx.Params(":action") {
case "watch":
- err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
case "unwatch":
- err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "star":
- err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
case "unstar":
- err = repo_model.StarRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "accept_transfer":
err = acceptOrRejectRepoTransfer(ctx, true)
case "reject_transfer":
@@ -335,8 +337,12 @@ func Action(ctx *context.Context) {
}
if err != nil {
- ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
- return
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
+ } else {
+ ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.Params(":action")), err)
+ return
+ }
}
switch ctx.Params(":action") {
@@ -365,7 +371,7 @@ func Action(ctx *context.Context) {
return
}
- ctx.RedirectToFirst(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
+ ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
}
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
@@ -541,9 +547,13 @@ func InitiateDownload(ctx *context.Context) {
// SearchRepo repositories via options
func SearchRepo(ctx *context.Context) {
+ page := ctx.FormInt("page")
+ if page <= 0 {
+ page = 1
+ }
opts := &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
- Page: ctx.FormInt("page"),
+ Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
},
Actor: ctx.Doer,
@@ -552,33 +562,33 @@ func SearchRepo(ctx *context.Context) {
PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
TeamID: ctx.FormInt64("team_id"),
TopicOnly: ctx.FormBool("topic"),
- Collaborate: util.OptionalBoolNone,
+ Collaborate: optional.None[bool](),
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
- Template: util.OptionalBoolNone,
+ Template: optional.None[bool](),
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
}
if ctx.FormString("template") != "" {
- opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
+ opts.Template = optional.Some(ctx.FormBool("template"))
}
if ctx.FormBool("exclusive") {
- opts.Collaborate = util.OptionalBoolFalse
+ opts.Collaborate = optional.Some(false)
}
mode := ctx.FormString("mode")
switch mode {
case "source":
- opts.Fork = util.OptionalBoolFalse
- opts.Mirror = util.OptionalBoolFalse
+ opts.Fork = optional.Some(false)
+ opts.Mirror = optional.Some(false)
case "fork":
- opts.Fork = util.OptionalBoolTrue
+ opts.Fork = optional.Some(true)
case "mirror":
- opts.Mirror = util.OptionalBoolTrue
+ opts.Mirror = optional.Some(true)
case "collaborative":
- opts.Mirror = util.OptionalBoolFalse
- opts.Collaborate = util.OptionalBoolTrue
+ opts.Mirror = optional.Some(false)
+ opts.Collaborate = optional.Some(true)
case "":
default:
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
@@ -586,11 +596,11 @@ func SearchRepo(ctx *context.Context) {
}
if ctx.FormString("archived") != "" {
- opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
+ opts.Archived = optional.Some(ctx.FormBool("archived"))
}
if ctx.FormString("is_private") != "" {
- opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
+ opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
}
sortMode := ctx.FormString("sort")
@@ -612,47 +622,36 @@ func SearchRepo(ctx *context.Context) {
}
}
- var err error
+ // To improve performance when only the count is requested
+ if ctx.FormBool("count_only") {
+ if count, err := repo_model.CountRepository(ctx, opts); err != nil {
+ log.Error("CountRepository: %v", err)
+ ctx.JSON(http.StatusInternalServerError, nil) // frontend JS doesn't handle error response (same as below)
+ } else {
+ ctx.SetTotalCountHeader(count)
+ ctx.JSONOK()
+ }
+ return
+ }
+
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
- ctx.JSON(http.StatusInternalServerError, api.SearchError{
- OK: false,
- Error: err.Error(),
- })
+ log.Error("SearchRepository: %v", err)
+ ctx.JSON(http.StatusInternalServerError, nil)
return
}
ctx.SetTotalCountHeader(count)
- // To improve performance when only the count is requested
- if ctx.FormBool("count_only") {
- return
- }
-
- // collect the latest commit of each repo
- // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
- repoBranchNames := make(map[int64]string, len(repos))
- for _, repo := range repos {
- repoBranchNames[repo.ID] = repo.DefaultBranch
- }
-
- repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
+ latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
if err != nil {
- log.Error("FindBranchesByRepoAndBranchName: %v", err)
- return
- }
-
- // call the database O(1) times to get the commit statuses for all repos
- repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoIDsToLatestCommitSHAs, db.ListOptionsAll)
- if err != nil {
- log.Error("GetLatestCommitStatusForPairs: %v", err)
+ log.Error("FindReposLastestCommitStatuses: %v", err)
+ ctx.JSON(http.StatusInternalServerError, nil)
return
}
results := make([]*repo_service.WebSearchRepository, len(repos))
for i, repo := range repos {
- latestCommitStatus := git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
-
results[i] = &repo_service.WebSearchRepository{
Repository: &api.Repository{
ID: repo.ID,
@@ -666,8 +665,11 @@ func SearchRepo(ctx *context.Context) {
Link: repo.Link(),
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
},
- LatestCommitStatus: latestCommitStatus,
- LocaleLatestCommitStatus: latestCommitStatus.LocaleString(ctx.Locale),
+ }
+
+ if latestCommitStatuses[i] != nil {
+ results[i].LatestCommitStatus = latestCommitStatuses[i]
+ results[i].LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(ctx.Locale)
}
}
@@ -685,10 +687,8 @@ type branchTagSearchResponse struct {
func GetBranchesList(ctx *context.Context) {
branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
- IsDeletedBranch: util.OptionalBoolFalse,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
+ IsDeletedBranch: optional.Some(false),
+ ListOptions: db.ListOptionsAll,
}
branches, err := git_model.FindBranchNames(ctx, branchOpts)
if err != nil {
@@ -720,10 +720,8 @@ func GetTagList(ctx *context.Context) {
func PrepareBranchList(ctx *context.Context) {
branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
- IsDeletedBranch: util.OptionalBoolFalse,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
+ IsDeletedBranch: optional.Some(false),
+ ListOptions: db.ListOptionsAll,
}
brs, err := git_model.FindBranchNames(ctx, branchOpts)
if err != nil {
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 3c0fa4bc00..46f0208453 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -5,31 +5,28 @@ package repo
import (
"net/http"
+ "strings"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/git"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
const tplSearch base.TplName = "repo/search"
// Search render repository search page
func Search(ctx *context.Context) {
- if !setting.Indexer.RepoIndexerEnabled {
- ctx.Redirect(ctx.Repo.RepoLink)
- return
- }
-
language := ctx.FormTrim("l")
keyword := ctx.FormTrim("q")
- queryType := ctx.FormTrim("t")
- isMatch := queryType == "match"
+ isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
ctx.Data["Keyword"] = keyword
ctx.Data["Language"] = language
- ctx.Data["queryType"] = queryType
+ ctx.Data["IsFuzzy"] = isFuzzy
ctx.Data["PageIsViewCode"] = true
if keyword == "" {
@@ -42,25 +39,61 @@ func Search(ctx *context.Context) {
page = 1
}
- total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
- language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
- if err != nil {
- if code_indexer.IsAvailable(ctx) {
- ctx.ServerError("SearchResults", err)
+ var total int
+ var searchResults []*code_indexer.Result
+ var searchResultLanguages []*code_indexer.SearchResultLanguages
+ if setting.Indexer.RepoIndexerEnabled {
+ var err error
+ total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+ RepoIDs: []int64{ctx.Repo.Repository.ID},
+ Keyword: keyword,
+ IsKeywordFuzzy: isFuzzy,
+ Language: language,
+ Paginator: &db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.RepoSearchPagingNum,
+ },
+ })
+ if err != nil {
+ if code_indexer.IsAvailable(ctx) {
+ ctx.ServerError("SearchResults", err)
+ return
+ }
+ ctx.Data["CodeIndexerUnavailable"] = true
+ } else {
+ ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
+ }
+ } else {
+ res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy})
+ if err != nil {
+ ctx.ServerError("GrepSearch", err)
return
}
- ctx.Data["CodeIndexerUnavailable"] = true
- } else {
- ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
+ total = len(res)
+ pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res))
+ pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res))
+ res = res[pageStart:pageEnd]
+ for _, r := range res {
+ searchResults = append(searchResults, &code_indexer.Result{
+ RepoID: ctx.Repo.Repository.ID,
+ Filename: r.Filename,
+ CommitID: ctx.Repo.CommitID,
+ // UpdatedUnix: not supported yet
+ // Language: not supported yet
+ // Color: not supported yet
+ Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
+ })
+ }
}
- ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
+ ctx.Data["CodeIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+ ctx.Data["Repo"] = ctx.Repo.Repository
ctx.Data["SearchResults"] = searchResults
ctx.Data["SearchResultLanguages"] = searchResultLanguages
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "l", "Language")
+ pager.AddParamString("l", language)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSearch)
diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go
index 02c807b775..504f57cfc2 100644
--- a/routers/web/repo/setting/avatar.go
+++ b/routers/web/repo/setting/avatar.go
@@ -8,11 +8,11 @@ import (
"fmt"
"io"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -38,7 +38,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
defer r.Close()
if form.Avatar.Size > setting.Avatar.MaxFileSize {
- return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
+ return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
}
data, err := io.ReadAll(r)
@@ -47,7 +47,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
- return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+ return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
}
if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil {
return fmt.Errorf("UploadAvatar: %w", err)
diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go
index c5c2a88c49..31f9f76d0f 100644
--- a/routers/web/repo/setting/collaboration.go
+++ b/routers/web/repo/setting/collaboration.go
@@ -4,19 +4,19 @@
package setting
import (
+ "errors"
"net/http"
"strings"
- "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"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/mailer"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
@@ -27,7 +27,7 @@ func Collaboration(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
ctx.Data["PageIsSettingsCollaboration"] = true
- users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
+ users, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: ctx.Repo.Repository.ID})
if err != nil {
ctx.ServerError("GetCollaborators", err)
return
@@ -101,7 +101,12 @@ func CollaborationPost(ctx *context.Context) {
}
if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
- ctx.ServerError("AddCollaborator", err)
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator.blocked_user"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
+ } else {
+ ctx.ServerError("AddCollaborator", err)
+ }
return
}
@@ -126,10 +131,19 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
// DeleteCollaboration delete a collaboration for a repository
func DeleteCollaboration(ctx *context.Context) {
- if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil {
- ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+ if collaborator, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
+ } else {
+ ctx.ServerError("GetUserByName", err)
+ return
+ }
} else {
- ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+ if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
+ ctx.Flash.Error("DeleteCollaboration: " + err.Error())
+ } else {
+ ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
+ }
}
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/collaboration")
diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go
index c8a576e576..881d148afc 100644
--- a/routers/web/repo/setting/default_branch.go
+++ b/routers/web/repo/setting/default_branch.go
@@ -7,10 +7,10 @@ import (
"net/http"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/web/repo"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go
index 3d4420006c..abc3eb4af1 100644
--- a/routers/web/repo/setting/deploy_key.go
+++ b/routers/web/repo/setting/deploy_key.go
@@ -8,11 +8,11 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go
index 551327d44b..217a01c90c 100644
--- a/routers/web/repo/setting/git_hooks.go
+++ b/routers/web/repo/setting/git_hooks.go
@@ -6,8 +6,8 @@ package setting
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/services/context"
)
// GitHooks hooks of a repository
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index cd0f11d548..6dddade066 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/lfs"
@@ -28,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -287,22 +287,19 @@ func LFSFileGet(ctx *context.Context) {
st := typesniffer.DetectContentType(buf)
ctx.Data["IsTextFile"] = st.IsText()
- isRepresentableAsText := st.IsRepresentableAsText()
-
- fileSize := meta.Size
ctx.Data["FileSize"] = meta.Size
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
switch {
- case isRepresentableAsText:
- if st.IsSvgImage() {
- ctx.Data["IsImageFile"] = true
- }
-
- if fileSize >= setting.UI.MaxDisplayFileSize {
+ case st.IsRepresentableAsText():
+ if meta.Size >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
break
}
+ if st.IsSvgImage() {
+ ctx.Data["IsImageFile"] = true
+ }
+
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
// Building code view blocks with line number on server side.
@@ -338,6 +335,8 @@ func LFSFileGet(ctx *context.Context) {
ctx.Data["IsAudioFile"] = true
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
ctx.Data["IsImageFile"] = true
+ default:
+ // TODO: the logic is not the same as "renderFile" in "view.go"
}
ctx.HTML(http.StatusOK, tplSettingsLFSFile)
}
@@ -388,7 +387,7 @@ func LFSFileFind(ctx *context.Context) {
sha := ctx.FormString("sha")
ctx.Data["Title"] = oid
ctx.Data["PageIsSettingsLFS"] = true
- objectFormat, _ := ctx.Repo.GitRepo.GetObjectFormat()
+ objectFormat := ctx.Repo.GetObjectFormat()
var objectID git.ObjectID
if len(sha) == 0 {
pointer := lfs.Pointer{Oid: oid, Size: size}
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index 98d6977b81..4bab3f897a 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -15,9 +15,9 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/services/repository"
@@ -68,7 +68,7 @@ func SettingsProtectedBranch(c *context.Context) {
}
c.Data["PageIsSettingsBranches"] = true
- c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName
+ c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
users, err := access_model.GetRepoReaders(c, c.Repo.Repository)
if err != nil {
@@ -313,7 +313,13 @@ func RenameBranchPost(ctx *context.Context) {
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
if err != nil {
- ctx.ServerError("RenameBranch", err)
+ switch {
+ case git_model.IsErrBranchAlreadyExists(err):
+ ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
+ ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ default:
+ ctx.ServerError("RenameBranch", err)
+ }
return
}
diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go
index 46addb3f0a..2c25b650b9 100644
--- a/routers/web/repo/setting/protected_tag.go
+++ b/routers/web/repo/setting/protected_tag.go
@@ -13,9 +13,9 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go
index 8d4112c157..a47d3b45e2 100644
--- a/routers/web/repo/setting/runners.go
+++ b/routers/web/repo/setting/runners.go
@@ -11,10 +11,10 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
actions_shared "code.gitea.io/gitea/routers/web/shared/actions"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go
index cf427b2c44..d4d56bfc57 100644
--- a/routers/web/repo/setting/secrets.go
+++ b/routers/web/repo/setting/secrets.go
@@ -8,10 +8,10 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/secrets"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 8c1daf52bc..00a5282f34 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -5,6 +5,7 @@
package setting
import (
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -12,25 +13,26 @@ import (
"time"
"code.gitea.io/gitea/models"
+ actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/indexer/stats"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
+ actions_service "code.gitea.io/gitea/services/actions"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
@@ -488,6 +490,13 @@ func SettingsPost(ctx *context.Context) {
}
}
+ if form.DefaultWikiBranch != "" {
+ if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
+ log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
+ ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
+ }
+ }
+
if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
@@ -534,6 +543,9 @@ func SettingsPost(ctx *context.Context) {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypeProjects,
+ Config: &repo_model.ProjectsConfig{
+ ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+ },
})
} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
@@ -576,6 +588,7 @@ func SettingsPost(ctx *context.Context) {
AllowRebase: form.PullsAllowRebase,
AllowRebaseMerge: form.PullsAllowRebaseMerge,
AllowSquash: form.PullsAllowSquash,
+ AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
AllowManualMerge: form.PullsAllowManualMerge,
AutodetectManualMerge: form.EnableAutodetectManualMerge,
AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
@@ -692,7 +705,7 @@ func SettingsPost(ctx *context.Context) {
}
repo.IsMirror = false
- if _, err := repo_module.CleanUpMigrateInfo(ctx, repo); err != nil {
+ if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
ctx.ServerError("CleanUpMigrateInfo", err)
return
} else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
@@ -779,6 +792,8 @@ func SettingsPost(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
} else if models.IsErrRepoTransferInProgress(err) {
ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
} else {
ctx.ServerError("TransferOwnership", err)
}
@@ -884,6 +899,10 @@ func SettingsPost(ctx *context.Context) {
return
}
+ if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+ log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
+
ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
@@ -902,6 +921,12 @@ func SettingsPost(ctx *context.Context) {
return
}
+ if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+ if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+ log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
+ }
+
ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index 066d2ef2a9..09586cc68d 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -14,10 +14,10 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
repo_service "code.gitea.io/gitea/services/repository"
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
index 428aa0bd5c..45b6c0f39a 100644
--- a/routers/web/repo/setting/variables.go
+++ b/routers/web/repo/setting/variables.go
@@ -8,10 +8,10 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
shared "code.gitea.io/gitea/routers/web/shared/actions"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index f9f41d0bd5..1a3549fea4 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -18,15 +18,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
webhook_module "code.gitea.io/gitea/modules/webhook"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
webhook_service "code.gitea.io/gitea/services/webhook"
@@ -152,6 +151,7 @@ func WebhooksNew(ctx *context.Context) {
}
}
ctx.Data["BaseLink"] = orCtx.LinkNew
+ ctx.Data["BaseLinkNew"] = orCtx.LinkNew
ctx.HTML(http.StatusOK, orCtx.NewTemplate)
}
@@ -588,6 +588,7 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
return nil, nil
}
ctx.Data["BaseLink"] = orCtx.Link
+ ctx.Data["BaseLinkNew"] = orCtx.LinkNew
var w *webhook.Webhook
if orCtx.RepoID > 0 {
@@ -656,12 +657,7 @@ func TestWebhook(ctx *context.Context) {
commit := ctx.Repo.Commit
if commit == nil {
ghost := user_model.NewGhostUser()
- objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository)
- if err != nil {
- ctx.Flash.Error("GetObjectFormatOfRepo: " + err.Error())
- ctx.Status(http.StatusInternalServerError)
- return
- }
+ objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
commit = &git.Commit{
ID: objectFormat.EmptyObjectID(),
Author: ghost.NewGitSig(),
diff --git a/routers/web/repo/topic.go b/routers/web/repo/topic.go
index d0e706c5bd..d81a695df9 100644
--- a/routers/web/repo/topic.go
+++ b/routers/web/repo/topic.go
@@ -8,8 +8,8 @@ import (
"strings"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
// TopicsPost response for creating repository
diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go
index c364e7090f..d11af4669f 100644
--- a/routers/web/repo/treelist.go
+++ b/routers/web/repo/treelist.go
@@ -7,8 +7,8 @@ import (
"net/http"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/services/context"
"github.com/go-enry/go-enry/v2"
)
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index af3021da11..8aa9dbb1be 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -35,8 +35,6 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/lfs"
@@ -45,10 +43,13 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
+ "code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
+ files_service "code.gitea.io/gitea/services/repository/files"
"github.com/nektos/act/pkg/model"
@@ -358,7 +359,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{ListAll: true})
+ statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
@@ -481,17 +482,17 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
switch {
case isRepresentableAsText:
+ if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+ ctx.Data["IsFileTooLarge"] = true
+ break
+ }
+
if fInfo.st.IsSvgImage() {
ctx.Data["IsImageFile"] = true
ctx.Data["CanCopyContent"] = true
ctx.Data["HasSourceRenderedToggle"] = true
}
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
-
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
shouldRenderSource := ctx.FormString("display") == "source"
@@ -553,31 +554,11 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
}
ctx.Data["NumLinesSet"] = true
- language := ""
-
- indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
- if err == nil {
- defer deleteTemporaryFile()
-
- filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
- CachedOnly: true,
- Attributes: []string{"linguist-language", "gitlab-language"},
- Filenames: []string{ctx.Repo.TreePath},
- IndexFile: indexFilename,
- WorkTree: worktree,
- })
- if err != nil {
- log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
- }
-
- language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
- if language == "" || language == "unspecified" {
- language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
- }
- if language == "unspecified" {
- language = ""
- }
+ language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+ if err != nil {
+ log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
+
fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
ctx.Data["LexerName"] = lexerName
if err != nil {
@@ -625,6 +606,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
break
}
+ // TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
+ // maybe for this case, the file is a binary file, and shouldn't be rendered?
if markupType := markup.Type(blob.Name()); markupType != "" {
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
ctx.Data["IsMarkup"] = true
@@ -653,11 +636,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
defer deferable()
attrs, err := checker.CheckPath(ctx.Repo.TreePath)
if err == nil {
- vendored, has := attrs["linguist-vendored"]
- ctx.Data["IsVendored"] = has && (vendored == "set" || vendored == "true")
-
- generated, has := attrs["linguist-generated"]
- ctx.Data["IsGenerated"] = has && (generated == "set" || generated == "true")
+ ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
+ ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
}
}
}
@@ -758,7 +738,7 @@ func checkHomeCodeViewable(ctx *context.Context) {
}
}
- ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
+ ctx.NotFound("Home", fmt.Errorf(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
}
func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
@@ -811,7 +791,7 @@ func Home(ctx *context.Context) {
return
}
- renderCode(ctx)
+ renderHomeCode(ctx)
}
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
@@ -880,25 +860,18 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
defer cancel()
}
- selected := make(container.Set[string])
- selected.AddMultiple(ctx.FormStrings("f[]")...)
-
- entries := allEntries
- if len(selected) > 0 {
- entries = make(git.Entries, 0, len(selected))
- for _, entry := range allEntries {
- if selected.Contains(entry.Name()) {
- entries = append(entries, entry)
- }
- }
- }
-
- var latestCommit *git.Commit
- ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
+ files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return nil
}
+ ctx.Data["Files"] = files
+ for _, f := range files {
+ if f.Commit == nil {
+ ctx.Data["HasFilesWithoutLatestCommit"] = true
+ break
+ }
+ }
if !loadLatestCommitData(ctx, latestCommit) {
return nil
@@ -928,7 +901,7 @@ func renderLanguageStats(ctx *context.Context) {
}
func renderRepoTopics(ctx *context.Context) {
- topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
+ topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@@ -938,9 +911,33 @@ func renderRepoTopics(ctx *context.Context) {
ctx.Data["Topics"] = topics
}
-func renderCode(ctx *context.Context) {
+func prepareOpenWithEditorApps(ctx *context.Context) {
+ var tmplApps []map[string]any
+ apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx)
+ if len(apps) == 0 {
+ apps = setting.DefaultOpenWithEditorApps()
+ }
+ for _, app := range apps {
+ schema, _, _ := strings.Cut(app.OpenURL, ":")
+ var iconHTML template.HTML
+ if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
+ iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2")
+ } else {
+ iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future
+ }
+ tmplApps = append(tmplApps, map[string]any{
+ "DisplayName": app.DisplayName,
+ "OpenURL": app.OpenURL,
+ "IconHTML": iconHTML,
+ })
+ }
+ ctx.Data["OpenWithEditorApps"] = tmplApps
+}
+
+func renderHomeCode(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled
+ prepareOpenWithEditorApps(ctx)
if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() {
showEmpty := true
@@ -1003,6 +1000,8 @@ func renderCode(ctx *context.Context) {
return
}
+ checkOutdatedBranch(ctx)
+
checkCitationFile(ctx, entry)
if ctx.Written() {
return
@@ -1069,6 +1068,31 @@ func renderCode(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRepoHome)
}
+func checkOutdatedBranch(ctx *context.Context) {
+ if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) {
+ return
+ }
+
+ // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName`
+ commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
+ if err != nil {
+ log.Error("GetBranchCommitID: %v", err)
+ // Don't return an error page, as it can be rechecked the next time the user opens the page.
+ return
+ }
+
+ dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName)
+ if err != nil {
+ log.Error("GetBranch: %v", err)
+ // Don't return an error page, as it can be rechecked the next time the user opens the page.
+ return
+ }
+
+ if dbBranch.CommitID != commit.ID.String() {
+ ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
+ }
+}
+
// RenderUserCards render a page show users according the input template
func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) {
page := ctx.FormInt("page")
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 5e7b971e67..df15f61b17 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
@@ -29,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
notify_service "code.gitea.io/gitea/services/notify"
wiki_service "code.gitea.io/gitea/services/wiki"
@@ -93,17 +93,32 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
}
func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
- wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
- if err != nil {
- ctx.ServerError("OpenRepository", err)
- return nil, nil, err
+ wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ if errGitRepo != nil {
+ ctx.ServerError("OpenRepository", errGitRepo)
+ return nil, nil, errGitRepo
}
- commit, err := wikiRepo.GetBranchCommit(wiki_service.DefaultBranch)
- if err != nil {
- return wikiRepo, nil, err
+ commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
+ if git.IsErrNotExist(errCommit) {
+ // if the default branch recorded in database is out of sync, then re-sync it
+ gitRepoDefaultBranch, errBranch := gitrepo.GetWikiDefaultBranch(ctx, ctx.Repo.Repository)
+ if errBranch != nil {
+ return wikiGitRepo, nil, errBranch
+ }
+ // update the default branch in the database
+ errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
+ if errDb != nil {
+ return wikiGitRepo, nil, errDb
+ }
+ ctx.Repo.Repository.DefaultWikiBranch = gitRepoDefaultBranch
+ // retry to get the commit from the correct default branch
+ commit, errCommit = wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
}
- return wikiRepo, commit, nil
+ if errCommit != nil {
+ return wikiGitRepo, nil, errCommit
+ }
+ return wikiGitRepo, commit, nil
}
// wikiContentsByEntry returns the contents of the wiki page referenced by the
@@ -316,7 +331,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
}
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename)
+ commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
return wikiRepo, entry
@@ -368,7 +383,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
ctx.Data["footerContent"] = ""
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename)
+ commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
// get page
@@ -380,7 +395,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
// get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
- Revision: wiki_service.DefaultBranch,
+ Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
Page: page,
})
@@ -402,20 +417,17 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
func renderEditPage(ctx *context.Context) {
wikiRepo, commit, err := findWikiRepoCommit(ctx)
- if err != nil {
+ defer func() {
if wikiRepo != nil {
- wikiRepo.Close()
+ _ = wikiRepo.Close()
}
+ }()
+ if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return
}
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
// get requested pagename
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
@@ -584,17 +596,15 @@ func WikiPages(ctx *context.Context) {
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
wikiRepo, commit, err := findWikiRepoCommit(ctx)
- if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- return
- }
defer func() {
if wikiRepo != nil {
- wikiRepo.Close()
+ _ = wikiRepo.Close()
}
}()
+ if err != nil {
+ ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
+ return
+ }
entries, err := commit.ListEntries()
if err != nil {
@@ -714,7 +724,7 @@ func NewWikiPost(ctx *context.Context) {
wikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(form.Message) == 0 {
- form.Message = ctx.Tr("repo.editor.add", form.Title)
+ form.Message = ctx.Locale.TrString("repo.editor.add", form.Title)
}
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
@@ -766,7 +776,7 @@ func EditWikiPost(ctx *context.Context) {
newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(form.Message) == 0 {
- form.Message = ctx.Tr("repo.editor.update", form.Title)
+ form.Message = ctx.Locale.TrString("repo.editor.update", form.Title)
}
if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index d3decdae2d..4602dcfeb4 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -9,12 +9,13 @@ import (
"net/url"
"testing"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
wiki_service "code.gitea.io/gitea/services/wiki"
@@ -79,7 +80,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) {
func TestWiki(t *testing.T) {
unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
+ ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
ctx.SetParams("*", "Home")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
@@ -144,7 +145,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
})
NewWikiPost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
- assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
+ assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
}
@@ -199,12 +200,13 @@ func TestDeleteWikiPagePost(t *testing.T) {
func TestWikiRaw(t *testing.T) {
for filepath, filetype := range map[string]string{
- "jpeg.jpg": "image/jpeg",
- "images/jpeg.jpg": "image/jpeg",
- "Page With Spaced Name": "text/plain; charset=utf-8",
- "Page-With-Spaced-Name": "text/plain; charset=utf-8",
- "Page With Spaced Name.md": "", // there is no "Page With Spaced Name.md" in repo
- "Page-With-Spaced-Name.md": "text/plain; charset=utf-8",
+ "jpeg.jpg": "image/jpeg",
+ "images/jpeg.jpg": "image/jpeg",
+ "files/Non-Renderable-File.zip": "application/octet-stream",
+ "Page With Spaced Name": "text/plain; charset=utf-8",
+ "Page-With-Spaced-Name": "text/plain; charset=utf-8",
+ "Page With Spaced Name.md": "", // there is no "Page With Spaced Name.md" in repo
+ "Page-With-Spaced-Name.md": "text/plain; charset=utf-8",
} {
unittest.PrepareTestEnv(t)
@@ -221,3 +223,38 @@ func TestWikiRaw(t *testing.T) {
}
}
}
+
+func TestDefaultWikiBranch(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+
+ // repo with no wiki
+ repoWithNoWiki := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ assert.False(t, repoWithNoWiki.HasWiki())
+ assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
+
+ // repo with wiki
+ assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
+
+ ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
+ ctx.SetParams("*", "Home")
+ contexttest.LoadRepo(t, ctx, 1)
+ assert.Equal(t, "wrong-branch", ctx.Repo.Repository.DefaultWikiBranch)
+ Wiki(ctx) // after the visiting, the out-of-sync database record will update the branch name to "master"
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.Equal(t, "master", ctx.Repo.Repository.DefaultWikiBranch)
+
+ // invalid branch name should fail
+ assert.Error(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "the bad name"))
+ repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.Equal(t, "master", repo.DefaultWikiBranch)
+
+ // the same branch name, should succeed (actually a no-op)
+ assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "master"))
+ repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.Equal(t, "master", repo.DefaultWikiBranch)
+
+ // change to another name
+ assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repo, "main"))
+ repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.Equal(t, "main", repo.DefaultWikiBranch)
+}
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index ae9a376724..34b7969442 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -8,10 +8,10 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index 07a0575207..79c03e4e8c 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -4,17 +4,13 @@
package actions
import (
- "errors"
- "regexp"
- "strings"
-
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
+ actions_service "code.gitea.io/gitea/services/actions"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
- secret_service "code.gitea.io/gitea/services/secrets"
)
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
@@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
ctx.Data["Variables"] = variables
}
-// some regular expression of `variables` and `secrets`
-// reference to:
-// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
-// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
-var (
- forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
-)
-
-func envNameCIRegexMatch(name string) error {
- if forbiddenEnvNameCIRx.MatchString(name) {
- log.Error("Env Name cannot be ci")
- return errors.New("env name cannot be ci")
- }
- return nil
-}
-
func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.EditVariableForm)
- if err := secret_service.ValidateName(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- if err := envNameCIRegexMatch(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
+ v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
if err != nil {
- log.Error("InsertVariable error: %v", err)
+ log.Error("CreateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
+
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
ctx.JSONRedirect(redirectURL)
}
@@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")
form := web.GetForm(ctx).(*forms.EditVariableForm)
- if err := secret_service.ValidateName(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- if err := envNameCIRegexMatch(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
- ID: id,
- Name: strings.ToUpper(form.Name),
- Data: ReserveLineBreakForTextarea(form.Data),
- })
- if err != nil || !ok {
- log.Error("UpdateVariable error: %v", err)
+ if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
+ log.Error("UpdateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
return
}
@@ -99,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
func DeleteVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")
- if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
+ if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
log.Error("Delete variable [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
return
@@ -107,12 +63,3 @@ func DeleteVariable(ctx *context.Context, redirectURL string) {
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
ctx.JSONRedirect(redirectURL)
}
-
-func ReserveLineBreakForTextarea(input string) string {
- // Since the content is from a form which is a textarea, the line endings are \r\n.
- // It's a standard behavior of HTML.
- // But we want to store them as \n like what GitHub does.
- // And users are unlikely to really need to keep the \r.
- // Other than this, we should respect the original content, even leading or trailing spaces.
- return strings.ReplaceAll(input, "\r\n", "\n")
-}
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index 30c25374d1..57671ad8f1 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -12,10 +12,10 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
cargo_service "code.gitea.io/gitea/services/packages/cargo"
container_service "code.gitea.io/gitea/services/packages/container"
@@ -157,7 +157,7 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
for _, p := range packages {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index c805da734a..3bd421f86a 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -6,10 +6,10 @@ package secrets
import (
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/web/shared/actions"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
- s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
+ s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
diff --git a/routers/web/shared/user/block.go b/routers/web/shared/user/block.go
new file mode 100644
index 0000000000..8a2357623f
--- /dev/null
+++ b/routers/web/shared/user/block.go
@@ -0,0 +1,76 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "errors"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ user_service "code.gitea.io/gitea/services/user"
+)
+
+func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
+ blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
+ BlockerID: blocker.ID,
+ })
+ if err != nil {
+ ctx.ServerError("FindBlockings", err)
+ return
+ }
+ if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
+ ctx.ServerError("LoadAttributes", err)
+ return
+ }
+ ctx.Data["UserBlocks"] = blocks
+}
+
+func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
+ form := web.GetForm(ctx).(*forms.BlockUserForm)
+ if ctx.HasError() {
+ ctx.ServerError("FormValidation", nil)
+ return
+ }
+
+ blockee, err := user_model.GetUserByName(ctx, form.Blockee)
+ if err != nil {
+ ctx.ServerError("GetUserByName", nil)
+ return
+ }
+
+ switch form.Action {
+ case "block":
+ if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
+ if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
+ } else {
+ ctx.ServerError("BlockUser", err)
+ return
+ }
+ }
+ case "unblock":
+ if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
+ if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
+ ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
+ } else {
+ ctx.ServerError("UnblockUser", err)
+ return
+ }
+ }
+ case "note":
+ block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+ if err != nil {
+ ctx.ServerError("GetBlocking", err)
+ return
+ }
+ if block != nil {
+ if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
+ ctx.ServerError("UpdateBlockingNote", err)
+ return
+ }
+ }
+ }
+}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index a2c0abb47e..7531e1ba26 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -4,6 +4,8 @@
package user
import (
+ "net/url"
+
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
@@ -11,14 +13,14 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
)
// prepareContextForCommonProfile store some common data into context data for user's profile related pages (including the nav menu)
@@ -36,8 +38,9 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
- ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL
-
+ if setting.Service.UserLocationMapURL != "" {
+ ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
+ }
// Show OpenID URIs
openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
if err != nil {
@@ -45,7 +48,6 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
return
}
ctx.Data["OpenIDs"] = openIDs
-
if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(&markup.RenderContext{
Metas: map[string]string{"mode": "document"},
@@ -84,6 +86,14 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
if _, ok := ctx.Data["NumFollowing"]; !ok {
_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
}
+
+ if ctx.Doer != nil {
+ if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
+ ctx.ServerError("GetBlocking", err)
+ } else {
+ ctx.Data["UserBlocking"] = block
+ }
+ }
}
func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
@@ -126,7 +136,7 @@ func LoadHeaderCount(ctx *context.Context) error {
Actor: ctx.Doer,
OwnerID: ctx.ContextUser.ID,
Private: ctx.IsSigned,
- Collaborate: util.OptionalBoolFalse,
+ Collaborate: optional.Some(false),
IncludeDescription: setting.UI.SearchRepoDescription,
})
if err != nil {
@@ -142,7 +152,7 @@ func LoadHeaderCount(ctx *context.Context) error {
}
projectCount, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
OwnerID: ctx.ContextUser.ID,
- IsClosed: util.OptionalBoolOf(false),
+ IsClosed: optional.Some(false),
Type: projectType,
})
if err != nil {
diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go
index 493c97aa67..fc39b504a9 100644
--- a/routers/web/swagger_json.go
+++ b/routers/web/swagger_json.go
@@ -4,22 +4,10 @@
package web
import (
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
)
-// tplSwaggerV1Json swagger v1 json template
-const tplSwaggerV1Json base.TplName = "swagger/v1_json"
-
// SwaggerV1Json render swagger v1 json
func SwaggerV1Json(ctx *context.Context) {
- t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil)
- if err != nil {
- ctx.ServerError("unable to find template", err)
- return
- }
- ctx.Resp.Header().Set("Content-Type", "application/json")
- if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
- ctx.ServerError("unable to execute template", err)
- }
+ ctx.JSONTemplate("swagger/v1_json")
}
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index 772cc38bea..04f510161d 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -9,8 +9,8 @@ import (
"code.gitea.io/gitea/models/avatars"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
+ "code.gitea.io/gitea/services/context"
)
func cacheableRedirect(ctx *context.Context, location string) {
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index ee514a7cfe..785c37b124 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -6,12 +6,13 @@ package user
import (
"net/http"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/setting"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -39,12 +40,11 @@ func CodeSearch(ctx *context.Context) {
language := ctx.FormTrim("l")
keyword := ctx.FormTrim("q")
- queryType := ctx.FormTrim("t")
- isMatch := queryType == "match"
+ isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
ctx.Data["Keyword"] = keyword
ctx.Data["Language"] = language
- ctx.Data["queryType"] = queryType
+ ctx.Data["IsFuzzy"] = isFuzzy
ctx.Data["IsCodePage"] = true
if keyword == "" {
@@ -75,7 +75,16 @@ func CodeSearch(ctx *context.Context) {
)
if len(repoIDs) > 0 {
- total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+ total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
+ RepoIDs: repoIDs,
+ Keyword: keyword,
+ IsKeywordFuzzy: isFuzzy,
+ Language: language,
+ Paginator: &db.ListOptions{
+ Page: page,
+ PageSize: setting.UI.RepoSearchPagingNum,
+ },
+ })
if err != nil {
if code_indexer.IsAvailable(ctx) {
ctx.ServerError("SearchResults", err)
@@ -113,7 +122,7 @@ func CodeSearch(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "l", "Language")
+ pager.AddParamString("l", language)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUserCode)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 44920817c9..ff6c2a6c36 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -24,15 +24,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
- context_service "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
@@ -85,7 +84,7 @@ func Dashboard(ctx *context.Context) {
page = 1
}
- ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
+ ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Locale.TrString("dashboard")
ctx.Data["PageIsDashboard"] = true
ctx.Data["PageIsNews"] = true
cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)
@@ -134,7 +133,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["Feeds"] = feeds
pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
- pager.AddParam(ctx, "date", "Date")
+ pager.AddParamString("date", date)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplDashboard)
@@ -162,8 +161,8 @@ func Milestones(ctx *context.Context) {
Private: true,
AllPublic: false, // Include also all public repositories of users and public organisations
AllLimited: false, // Include also all public repositories of limited organisations
- Archived: util.OptionalBoolFalse,
- HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
+ Archived: optional.Some(false),
+ HasMilestones: optional.Some(true), // Just needs display repos has milestones
}
if ctxUser.IsOrganization() && ctx.Org.Team != nil {
@@ -215,7 +214,7 @@ func Milestones(ctx *context.Context) {
counts, err := issues_model.CountMilestonesMap(ctx, issues_model.FindMilestoneOptions{
RepoCond: userRepoCond,
Name: keyword,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: optional.Some(isShowClosed),
})
if err != nil {
ctx.ServerError("CountMilestonesByRepoIDs", err)
@@ -228,7 +227,7 @@ func Milestones(ctx *context.Context) {
PageSize: setting.UI.IssuePagingNum,
},
RepoCond: repoCond,
- IsClosed: util.OptionalBoolOf(isShowClosed),
+ IsClosed: optional.Some(isShowClosed),
SortType: sortType,
Name: keyword,
})
@@ -296,17 +295,17 @@ func Milestones(ctx *context.Context) {
}
}
- showRepoIds := make(container.Set[int64], len(showRepos))
+ showRepoIDs := make(container.Set[int64], len(showRepos))
for _, repo := range showRepos {
if repo.ID > 0 {
- showRepoIds.Add(repo.ID)
+ showRepoIDs.Add(repo.ID)
}
}
if len(repoIDs) == 0 {
- repoIDs = showRepoIds.Values()
+ repoIDs = showRepoIDs.Values()
}
repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool {
- return !showRepoIds.Contains(v)
+ return !showRepoIDs.Contains(v)
})
var pagerCount int
@@ -330,10 +329,10 @@ func Milestones(ctx *context.Context) {
ctx.Data["IsShowClosed"] = isShowClosed
pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
- pager.AddParam(ctx, "q", "Keyword")
- pager.AddParam(ctx, "repos", "RepoIDs")
- pager.AddParam(ctx, "sort", "SortType")
- pager.AddParam(ctx, "state", "State")
+ pager.AddParamString("q", keyword)
+ pager.AddParamString("repos", reposQuery)
+ pager.AddParamString("sort", sortType)
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplMilestones)
@@ -440,9 +439,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
isPullList := unitType == unit.TypePullRequests
opts := &issues_model.IssuesOptions{
- IsPull: util.OptionalBoolOf(isPullList),
+ IsPull: optional.Some(isPullList),
SortType: sortType,
- IsArchived: util.OptionalBoolFalse,
+ IsArchived: optional.Some(false),
Org: org,
Team: team,
User: ctx.Doer,
@@ -466,9 +465,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
Private: true,
AllPublic: false,
AllLimited: false,
- Collaborate: util.OptionalBoolNone,
+ Collaborate: optional.None[bool](),
UnitType: unitType,
- Archived: util.OptionalBoolFalse,
+ Archived: optional.Some(false),
}
if team != nil {
repoOpts.TeamID = team.ID
@@ -516,7 +515,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Educated guess: Do or don't show closed issues.
isShowClosed := ctx.FormString("state") == "closed"
- opts.IsClosed = util.OptionalBoolOf(isShowClosed)
+ opts.IsClosed = optional.Some(isShowClosed)
// Make sure page number is at least 1. Will be posted to ctx.Data.
page := ctx.FormInt("page")
@@ -530,17 +529,14 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Get IDs for labels (a filter option for issues/pulls).
// Required for IssuesOptions.
- var labelIDs []int64
selectedLabels := ctx.FormString("labels")
if len(selectedLabels) > 0 && selectedLabels != "0" {
var err error
- labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
+ opts.LabelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
if err != nil {
- ctx.ServerError("StringsToInt64s", err)
- return
+ ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
}
}
- opts.LabelIDs = labelIDs
// ------------------------------
// Get issues as defined by opts.
@@ -633,13 +629,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
- pager.AddParam(ctx, "q", "Keyword")
- pager.AddParam(ctx, "type", "ViewType")
- pager.AddParam(ctx, "sort", "SortType")
- pager.AddParam(ctx, "state", "State")
- pager.AddParam(ctx, "labels", "SelectLabels")
- pager.AddParam(ctx, "milestone", "MilestoneID")
- pager.AddParam(ctx, "assignee", "AssigneeID")
+ pager.AddParamString("q", keyword)
+ pager.AddParamString("type", viewType)
+ pager.AddParamString("sort", sortType)
+ pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
+ pager.AddParamString("labels", selectedLabels)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplIssues)
@@ -714,13 +708,17 @@ func UsernameSubRoute(ctx *context.Context) {
username := ctx.Params("username")
reloadParam := func(suffix string) (success bool) {
ctx.SetParams("username", strings.TrimSuffix(username, suffix))
- context_service.UserAssignmentWeb()(ctx)
+ context.UserAssignmentWeb()(ctx)
+ if ctx.Written() {
+ return false
+ }
+
// check view permissions
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
return false
}
- return !ctx.Written()
+ return true
}
switch {
case strings.HasSuffix(username, ".png"):
@@ -741,7 +739,6 @@ func UsernameSubRoute(ctx *context.Context) {
return
}
if reloadParam(".rss") {
- context_service.UserAssignmentWeb()(ctx)
feed.ShowUserFeedRSS(ctx)
}
case strings.HasSuffix(username, ".atom"):
@@ -753,7 +750,7 @@ func UsernameSubRoute(ctx *context.Context) {
feed.ShowUserFeedAtom(ctx)
}
default:
- context_service.UserAssignmentWeb()(ctx)
+ context.UserAssignmentWeb()(ctx)
if !ctx.Written() {
ctx.Data["EnableFeed"] = setting.Other.EnableFeed
OwnerProfile(ctx)
@@ -790,22 +787,22 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
case issues_model.FilterModeYourRepositories:
openClosedOpts.AllPublic = false
case issues_model.FilterModeAssign:
- openClosedOpts.AssigneeID = &doerID
+ openClosedOpts.AssigneeID = optional.Some(doerID)
case issues_model.FilterModeCreate:
- openClosedOpts.PosterID = &doerID
+ openClosedOpts.PosterID = optional.Some(doerID)
case issues_model.FilterModeMention:
- openClosedOpts.MentionID = &doerID
+ openClosedOpts.MentionID = optional.Some(doerID)
case issues_model.FilterModeReviewRequested:
- openClosedOpts.ReviewRequestedID = &doerID
+ openClosedOpts.ReviewRequestedID = optional.Some(doerID)
case issues_model.FilterModeReviewed:
- openClosedOpts.ReviewedID = &doerID
+ openClosedOpts.ReviewedID = optional.Some(doerID)
}
- openClosedOpts.IsClosed = util.OptionalBoolFalse
+ openClosedOpts.IsClosed = optional.Some(false)
ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
if err != nil {
return nil, err
}
- openClosedOpts.IsClosed = util.OptionalBoolTrue
+ openClosedOpts.IsClosed = optional.Some(true)
ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
if err != nil {
return nil, err
@@ -816,23 +813,23 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
if err != nil {
return nil, err
}
- ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID }))
+ ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) }))
if err != nil {
return nil, err
}
- ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID }))
+ ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) }))
if err != nil {
return nil, err
}
- ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID }))
+ ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = optional.Some(doerID) }))
if err != nil {
return nil, err
}
- ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID }))
+ ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = optional.Some(doerID) }))
if err != nil {
return nil, err
}
- ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID }))
+ ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = optional.Some(doerID) }))
if err != nil {
return nil, err
}
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index a32b015cd1..1cc9886308 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -10,8 +10,10 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -113,3 +115,18 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
assert.Len(t, ctx.Data["Milestones"], 1)
assert.Len(t, ctx.Data["Repos"], 2) // both repo 42 and 1 have milestones and both are owned by user 2
}
+
+func TestDashboardPagination(t *testing.T) {
+ ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ page := context.NewPagination(10, 3, 1, 3)
+
+ setting.AppSubURL = "/SubPath"
+ out, err := ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
+ assert.NoError(t, err)
+ assert.Contains(t, out, ``)
+
+ setting.AppSubURL = ""
+ out, err = ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page})
+ assert.NoError(t, err)
+ assert.Contains(t, out, ``)
+}
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 26f77cfc3a..ae0132e6e2 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -16,11 +16,12 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
)
@@ -143,6 +144,12 @@ func getNotifications(ctx *context.Context) {
ctx.ServerError("LoadIssues", err)
return
}
+
+ if err = notifications.LoadIssuePullRequests(ctx); err != nil {
+ ctx.ServerError("LoadIssuePullRequests", err)
+ return
+ }
+
notifications = notifications.Without(failures)
failCount += len(failures)
@@ -232,26 +239,25 @@ func NotificationSubscriptions(ctx *context.Context) {
if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) {
state = "all"
}
+
ctx.Data["State"] = state
- var showClosed util.OptionalBool
+ // default state filter is "all"
+ showClosed := optional.None[bool]()
switch state {
- case "all":
- showClosed = util.OptionalBoolNone
case "closed":
- showClosed = util.OptionalBoolTrue
+ showClosed = optional.Some(true)
case "open":
- showClosed = util.OptionalBoolFalse
+ showClosed = optional.Some(false)
}
- var issueTypeBool util.OptionalBool
issueType := ctx.FormString("issueType")
+ // default issue type is no filter
+ issueTypeBool := optional.None[bool]()
switch issueType {
case "issues":
- issueTypeBool = util.OptionalBoolFalse
+ issueTypeBool = optional.Some(false)
case "pulls":
- issueTypeBool = util.OptionalBoolTrue
- default:
- issueTypeBool = util.OptionalBoolNone
+ issueTypeBool = optional.Some(true)
}
ctx.Data["IssueType"] = issueType
@@ -262,8 +268,7 @@ func NotificationSubscriptions(ctx *context.Context) {
var err error
labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
if err != nil {
- ctx.ServerError("StringsToInt64s", err)
- return
+ ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
}
}
@@ -344,8 +349,8 @@ func NotificationSubscriptions(ctx *context.Context) {
ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
return
}
- pager.AddParam(ctx, "sort", "SortType")
- pager.AddParam(ctx, "state", "State")
+ pager.AddParamString("sort", sortType)
+ pager.AddParamString("state", state)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
@@ -389,6 +394,21 @@ func NotificationWatching(ctx *context.Context) {
orderBy = db.SearchOrderByRecentUpdated
}
+ archived := ctx.FormOptionalBool("archived")
+ ctx.Data["IsArchived"] = archived
+
+ fork := ctx.FormOptionalBool("fork")
+ ctx.Data["IsFork"] = fork
+
+ mirror := ctx.FormOptionalBool("mirror")
+ ctx.Data["IsMirror"] = mirror
+
+ template := ctx.FormOptionalBool("template")
+ ctx.Data["IsTemplate"] = template
+
+ private := ctx.FormOptionalBool("private")
+ ctx.Data["IsPrivate"] = private
+
repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
@@ -399,9 +419,14 @@ func NotificationWatching(ctx *context.Context) {
OrderBy: orderBy,
Private: ctx.IsSigned,
WatchedByID: ctx.Doer.ID,
- Collaborate: util.OptionalBoolFalse,
+ Collaborate: optional.Some(false),
TopicOnly: ctx.FormBool("topic"),
IncludeDescription: setting.UI.SearchRepoDescription,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 708af3e43c..9af49406c4 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -15,8 +15,8 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
debian_module "code.gitea.io/gitea/modules/packages/debian"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
@@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/web"
packages_helper "code.gitea.io/gitea/routers/api/packages/helper"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -54,7 +55,7 @@ func ListPackages(ctx *context.Context) {
OwnerID: ctx.ContextUser.ID,
Type: packages_model.Type(packageType),
Name: packages_model.SearchValue{Value: query},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
ctx.ServerError("SearchLatestVersions", err)
@@ -124,8 +125,8 @@ func ListPackages(ctx *context.Context) {
}
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
- pager.AddParam(ctx, "q", "Query")
- pager.AddParam(ctx, "type", "PackageType")
+ pager.AddParamString("q", query)
+ pager.AddParamString("type", packageType)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplPackagesList)
@@ -145,7 +146,7 @@ func RedirectToLastVersion(ctx *context.Context) {
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
ctx.ServerError("GetPackageByName", err)
@@ -162,7 +163,7 @@ func RedirectToLastVersion(ctx *context.Context) {
return
}
- ctx.Redirect(pd.FullWebLink())
+ ctx.Redirect(pd.VersionWebLink())
}
// ViewPackageVersion displays a single package version
@@ -255,7 +256,7 @@ func ViewPackageVersion(ctx *context.Context) {
pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
}
if err != nil {
@@ -359,7 +360,7 @@ func ListPackageVersions(ctx *context.Context) {
ExactMatch: false,
Value: query,
},
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Sort: sort,
})
if err != nil {
@@ -467,7 +468,7 @@ func PackageSettingsPost(ctx *context.Context) {
redirectURL := ctx.Package.Owner.HomeLink() + "/-/packages"
// redirect to the package if there are still versions available
- if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: util.OptionalBoolFalse}); has {
+ if has, _ := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{PackageID: ctx.Package.Descriptor.Package.ID, IsInternal: optional.Some(false)}); has {
redirectURL = ctx.Package.Descriptor.PackageWebLink()
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 73ab93caed..f0749e1021 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -15,20 +15,22 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/feed"
"code.gitea.io/gitea/routers/web/org"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
)
const (
tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar"
+ tplFollowUnfollow base.TplName = "org/follow_unfollow"
)
// OwnerProfile render profile page for a user or a organization (aka, repo owner)
@@ -160,6 +162,21 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
}
ctx.Data["NumFollowing"] = numFollowing
+ archived := ctx.FormOptionalBool("archived")
+ ctx.Data["IsArchived"] = archived
+
+ fork := ctx.FormOptionalBool("fork")
+ ctx.Data["IsFork"] = fork
+
+ mirror := ctx.FormOptionalBool("mirror")
+ ctx.Data["IsMirror"] = mirror
+
+ template := ctx.FormOptionalBool("template")
+ ctx.Data["IsTemplate"] = template
+
+ private := ctx.FormOptionalBool("private")
+ ctx.Data["IsPrivate"] = private
+
switch tab {
case "followers":
ctx.Data["Cards"] = followers
@@ -202,10 +219,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
OrderBy: orderBy,
Private: ctx.IsSigned,
StarredByID: ctx.ContextUser.ID,
- Collaborate: util.OptionalBoolFalse,
+ Collaborate: optional.Some(false),
TopicOnly: topicOnly,
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -224,10 +246,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
OrderBy: orderBy,
Private: ctx.IsSigned,
WatchedByID: ctx.ContextUser.ID,
- Collaborate: util.OptionalBoolFalse,
+ Collaborate: optional.Some(false),
TopicOnly: topicOnly,
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -269,10 +296,15 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
OwnerID: ctx.ContextUser.ID,
OrderBy: orderBy,
Private: ctx.IsSigned,
- Collaborate: util.OptionalBoolFalse,
+ Collaborate: optional.Some(false),
TopicOnly: topicOnly,
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
+ Archived: archived,
+ Fork: fork,
+ Mirror: mirror,
+ Template: template,
+ IsPrivate: private,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
@@ -292,12 +324,14 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
pager := context.NewPagination(total, pagingNum, page, 5)
pager.SetDefaultParams(ctx)
- pager.AddParam(ctx, "tab", "TabName")
+ pager.AddParamString("tab", tab)
if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
- pager.AddParam(ctx, "language", "Language")
+ pager.AddParamString("language", language)
}
if tab == "activity" {
- pager.AddParam(ctx, "date", "Date")
+ if ctx.Data["Date"] != nil {
+ pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"]))
+ }
}
ctx.Data["Page"] = pager
}
@@ -307,7 +341,7 @@ func Action(ctx *context.Context) {
var err error
switch ctx.FormString("action") {
case "follow":
- err = user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+ err = user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser)
case "unfollow":
err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
}
@@ -318,6 +352,16 @@ func Action(ctx *context.Context) {
return
}
- shared_user.PrepareContextForProfileBigAvatar(ctx)
- ctx.HTML(http.StatusOK, tplProfileBigAvatar)
+ if ctx.ContextUser.IsIndividual() {
+ shared_user.PrepareContextForProfileBigAvatar(ctx)
+ ctx.HTML(http.StatusOK, tplProfileBigAvatar)
+ return
+ } else if ctx.ContextUser.IsOrganization() {
+ ctx.Data["Org"] = ctx.ContextUser
+ ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+ ctx.HTML(http.StatusOK, tplFollowUnfollow)
+ return
+ }
+ log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
+ ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
}
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index 4d090a3784..fb7729bbe1 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -8,7 +8,7 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index c7f194a3b5..8ea7548e51 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -13,13 +13,15 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/auth"
+ "code.gitea.io/gitea/services/auth/source/db"
+ "code.gitea.io/gitea/services/auth/source/smtp"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
"code.gitea.io/gitea/services/user"
@@ -72,7 +74,6 @@ func AccountPost(ctx *context.Context) {
case errors.Is(err, password.ErrIsPwned):
ctx.Flash.Error(ctx.Tr("auth.password_pwned"))
case password.IsErrIsPwnedRequest(err):
- log.Error("%s", err.Error())
ctx.Flash.Error(ctx.Tr("auth.password_pwned_err"))
default:
ctx.ServerError("UpdateAuth", err)
@@ -92,9 +93,9 @@ func EmailPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
- // Make emailaddress primary.
+ // Make email address primary.
if ctx.FormString("_method") == "PRIMARY" {
- if err := user_model.MakeEmailPrimary(ctx, &user_model.EmailAddress{ID: ctx.FormInt64("id")}); err != nil {
+ if err := user_model.MakeActiveEmailPrimary(ctx, ctx.FormInt64("id")); err != nil {
ctx.ServerError("MakeEmailPrimary", err)
return
}
@@ -233,15 +234,33 @@ func DeleteEmail(ctx *context.Context) {
// DeleteAccount render user suicide page and response for delete user himself
func DeleteAccount(ctx *context.Context) {
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureDeletion) {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil {
- if user_model.IsErrUserNotExist(err) {
+ switch {
+ case user_model.IsErrUserNotExist(err):
+ loadAccountData(ctx)
+
+ ctx.RenderWithErr(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil)
+ case errors.Is(err, smtp.ErrUnsupportedLoginType):
+ loadAccountData(ctx)
+
+ ctx.RenderWithErr(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil)
+ case errors.As(err, &db.ErrUserPasswordNotSet{}):
+ loadAccountData(ctx)
+
+ ctx.RenderWithErr(ctx.Tr("form.unset_password"), tplSettingsAccount, nil)
+ case errors.As(err, &db.ErrUserPasswordInvalid{}):
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil)
- } else {
+ default:
ctx.ServerError("UserSignIn", err)
}
return
@@ -299,6 +318,7 @@ func loadAccountData(ctx *context.Context) {
ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
+ ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if setting.Service.UserDeleteWithCommentsMaxTime != 0 {
ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String()
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
index 6742c382e9..9fdc5e4d53 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -8,9 +8,9 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert"
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
index decb35c1e1..171c1933d4 100644
--- a/routers/web/user/setting/adopt.go
+++ b/routers/web/user/setting/adopt.go
@@ -8,9 +8,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index a7e31fd505..e3822ca988 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -10,9 +10,9 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/user/setting/block.go b/routers/web/user/setting/block.go
new file mode 100644
index 0000000000..94fc380cee
--- /dev/null
+++ b/routers/web/user/setting/block.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/setting"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
+)
+
+const (
+ tplSettingsBlockedUsers base.TplName = "user/settings/blocked_users"
+)
+
+func BlockedUsers(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("user.block.list")
+ ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+ shared_user.BlockedUsers(ctx, ctx.Doer)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
+}
+
+func BlockedUsersPost(ctx *context.Context) {
+ shared_user.BlockedUsersPost(ctx, ctx.Doer)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
+}
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 16410d06ff..9e969e045d 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -5,15 +5,17 @@
package setting
import (
+ "fmt"
"net/http"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -61,7 +63,7 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
return
}
- if _, err = asymkey_model.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
+ if _, err = asymkey_service.AddPrincipalKey(ctx, ctx.Doer.ID, content, 0); err != nil {
ctx.Data["HasPrincipalError"] = true
switch {
case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
@@ -77,6 +79,11 @@ func KeysPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "gpg":
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ return
+ }
+
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
@@ -153,6 +160,11 @@ func KeysPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "ssh":
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ return
+ }
+
content, err := asymkey_model.CheckPublicKeyString(form.Content)
if err != nil {
if db.IsErrSSHDisabled(err) {
@@ -192,6 +204,11 @@ func KeysPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_ssh":
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ return
+ }
+
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
@@ -224,12 +241,21 @@ func KeysPost(ctx *context.Context) {
func DeleteKey(ctx *context.Context) {
switch ctx.FormString("type") {
case "gpg":
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ return
+ }
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteGPGKey: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
}
case "ssh":
+ if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ return
+ }
+
keyID := ctx.FormInt64("id")
external, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, keyID)
if err != nil {
@@ -308,4 +334,5 @@ func loadKeysData(ctx *context.Context) {
ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
+ ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
}
diff --git a/routers/web/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go
index 93142c21fc..1f485e06c8 100644
--- a/routers/web/user/setting/oauth2.go
+++ b/routers/web/user/setting/oauth2.go
@@ -5,8 +5,8 @@ package setting
import (
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go
index fecaa4b873..85d1e820a5 100644
--- a/routers/web/user/setting/oauth2_common.go
+++ b/routers/web/user/setting/oauth2_common.go
@@ -9,10 +9,10 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go
index 34d18f999e..4132659495 100644
--- a/routers/web/user/setting/packages.go
+++ b/routers/web/user/setting/packages.go
@@ -9,11 +9,11 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
shared "code.gitea.io/gitea/routers/web/shared/packages"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 95b350528c..49eb050dcb 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -20,7 +20,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@@ -29,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
user_service "code.gitea.io/gitea/services/user"
)
@@ -126,7 +126,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
defer fr.Close()
if form.Avatar.Size > setting.Avatar.MaxFileSize {
- return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
+ return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
}
data, err := io.ReadAll(fr)
@@ -136,7 +136,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
- return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
+ return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
}
if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil {
return fmt.Errorf("UploadAvatar: %w", err)
@@ -389,7 +389,7 @@ func UpdateUserLang(ctx *context.Context) {
middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0)
log.Trace("User settings updated: %s", ctx.Doer.Name)
- ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_language_success"))
+ ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).TrString("settings.update_language_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
diff --git a/routers/web/user/setting/runner.go b/routers/web/user/setting/runner.go
index 451fd0ca97..2bb10cceb9 100644
--- a/routers/web/user/setting/runner.go
+++ b/routers/web/user/setting/runner.go
@@ -4,8 +4,8 @@
package setting
import (
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index 7858b634ce..cd09102369 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -13,10 +13,10 @@ import (
"strings"
"code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"github.com/pquerna/otp"
diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go
index 9a207e149d..8f788e1735 100644
--- a/routers/web/user/setting/security/openid.go
+++ b/routers/web/user/setting/security/openid.go
@@ -8,10 +8,10 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/openid"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index 3647d606ee..8d6859ab87 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -12,10 +12,10 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/context"
)
const (
@@ -112,7 +112,7 @@ func loadSecurityData(ctx *context.Context) {
ctx.Data["AccountLinks"] = sources
authSources, err := db.Find[auth_model.Source](ctx, auth_model.FindSourcesOptions{
- IsActive: util.OptionalBoolNone,
+ IsActive: optional.None[bool](),
LoginType: auth_model.OAuth2,
})
if err != nil {
diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go
index ce103528c5..e382c8b9af 100644
--- a/routers/web/user/setting/security/webauthn.go
+++ b/routers/web/user/setting/security/webauthn.go
@@ -11,10 +11,10 @@ import (
"code.gitea.io/gitea/models/auth"
wa "code.gitea.io/gitea/modules/auth/webauthn"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"github.com/go-webauthn/webauthn/protocol"
diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go
index 679b72e501..4423b62781 100644
--- a/routers/web/user/setting/webhooks.go
+++ b/routers/web/user/setting/webhooks.go
@@ -9,8 +9,8 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
const (
diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go
index 86f66e64a6..38f74ea455 100644
--- a/routers/web/user/stop_watch.go
+++ b/routers/web/user/stop_watch.go
@@ -8,7 +8,7 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index f35f40e6a0..8476767e9e 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -8,8 +8,8 @@ import (
"strconv"
admin_model "code.gitea.io/gitea/models/admin"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/services/context"
)
// TaskStatus returns task's status
@@ -39,7 +39,7 @@ func TaskStatus(ctx *context.Context) {
Args: []any{task.Message},
}
}
- message = ctx.Tr(translatableMessage.Format, translatableMessage.Args...)
+ message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
}
ctx.JSON(http.StatusOK, map[string]any{
diff --git a/routers/web/web.go b/routers/web/web.go
index 92cf5132b4..4fff994e42 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -12,7 +12,6 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/metrics"
"code.gitea.io/gitea/modules/public"
@@ -42,7 +41,7 @@ import (
user_setting "code.gitea.io/gitea/routers/web/user/setting"
"code.gitea.io/gitea/routers/web/user/setting/security"
auth_service "code.gitea.io/gitea/services/auth"
- context_service "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/lfs"
@@ -155,7 +154,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
if ctx.Doer.MustChangePassword {
if ctx.Req.URL.Path != "/user/settings/change_password" {
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
- ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password"))
+ ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
return
}
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
@@ -175,7 +174,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
// Redirect to dashboard (or alternate location) if user tries to visit any non-login page.
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
- ctx.RedirectToFirst(ctx.FormString("redirect_to"))
+ ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"))
return
}
@@ -473,6 +472,7 @@ func registerRoutes(m *web.Route) {
m.Get("/change-password", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
})
+ m.Get("/passkey-endpoints", passkeyEndpoints)
m.Methods("GET, HEAD", "/*", public.FileHandlerFunc())
}, optionsCorsHandler())
@@ -647,6 +647,11 @@ func registerRoutes(m *web.Route) {
})
addWebhookEditRoutes()
}, webhooksEnabled)
+
+ m.Group("/blocked_users", func() {
+ m.Get("", user_setting.BlockedUsers)
+ m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
+ })
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
m.Group("/user", func() {
@@ -676,6 +681,7 @@ func registerRoutes(m *web.Route) {
// ***** START: Admin *****
m.Group("/admin", func() {
m.Get("", admin.Dashboard)
+ m.Get("/system_status", admin.SystemStatus)
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
m.Get("/self_check", admin.SelfCheck)
@@ -684,6 +690,7 @@ func registerRoutes(m *web.Route) {
m.Get("", admin.Config)
m.Post("", admin.ChangeConfig)
m.Post("/test_mail", admin.SendTestMail)
+ m.Get("/settings", admin.ConfigSettings)
})
m.Group("/monitor", func() {
@@ -787,7 +794,7 @@ func registerRoutes(m *web.Route) {
m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment)
}, ignSignIn)
- m.Post("/{username}", reqSignIn, context_service.UserAssignmentWeb(), user.Action)
+ m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.Action)
reqRepoAdmin := context.RequireRepoAdmin()
reqRepoCodeWriter := context.RequireRepoWriter(unit.TypeCode)
@@ -943,6 +950,11 @@ func registerRoutes(m *web.Route) {
m.Post("/rebuild", org.RebuildCargoIndex)
})
}, packagesEnabled)
+
+ m.Group("/blocked_users", func() {
+ m.Get("", org.BlockedUsers)
+ m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
+ })
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(true, true))
}, reqSignIn)
@@ -954,10 +966,6 @@ func registerRoutes(m *web.Route) {
m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
m.Get("/migrate", repo.Migrate)
m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
- m.Group("/fork", func() {
- m.Combo("/{repoid}").Get(repo.Fork).
- Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
- }, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
m.Get("/search", repo.SearchRepo)
}, reqSignIn)
@@ -1000,7 +1008,6 @@ func registerRoutes(m *web.Route) {
m.Put("", web.Bind(forms.EditProjectBoardForm{}), org.EditProjectBoard)
m.Delete("", org.DeleteProjectBoard)
m.Post("/default", org.SetDefaultProjectBoard)
- m.Post("/unsetdefault", org.UnsetDefaultProjectBoard)
m.Post("/move", org.MoveIssues)
})
@@ -1016,7 +1023,7 @@ func registerRoutes(m *web.Route) {
m.Group("", func() {
m.Get("/code", user.CodeSearch)
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker)
- }, ignSignIn, context_service.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
+ }, ignSignIn, context.UserAssignmentWeb(), context.OrgAssignment()) // for "/{username}/-" (packages, projects, code)
m.Group("/{username}/{reponame}", func() {
m.Group("/settings", func() {
@@ -1253,6 +1260,8 @@ func registerRoutes(m *web.Route) {
m.Post("/delete", repo.DeleteBranchPost)
m.Post("/restore", repo.RestoreBranchPost)
}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+
+ m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
}, reqSignIn, context.RepoAssignment, context.UnitTypes())
// Tags
@@ -1338,13 +1347,12 @@ func registerRoutes(m *web.Route) {
m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
m.Delete("", repo.DeleteProjectBoard)
m.Post("/default", repo.SetDefaultProjectBoard)
- m.Post("/unsetdefault", repo.UnSetDefaultProjectBoard)
m.Post("/move", repo.MoveIssues)
})
})
}, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
- }, reqRepoProjectsReader, repo.MustEnableProjects)
+ }, reqRepoProjectsReader, repo.MustEnableRepoProjects)
m.Group("/actions", func() {
m.Get("", actions.List)
@@ -1364,10 +1372,14 @@ func registerRoutes(m *web.Route) {
})
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
- m.Post("/artifacts", actions.ArtifactsView)
+ m.Get("/artifacts", actions.ArtifactsView)
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
+ m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
})
+ m.Group("/workflows/{workflow_name}", func() {
+ m.Get("/badge.svg", actions.GetWorkflowBadge)
+ })
}, reqRepoActionsReader, actions.MustEnableActions)
m.Group("/wiki", func() {
@@ -1391,6 +1403,18 @@ func registerRoutes(m *web.Route) {
m.Group("/activity", func() {
m.Get("", repo.Activity)
m.Get("/{period}", repo.Activity)
+ m.Group("/contributors", func() {
+ m.Get("", repo.Contributors)
+ m.Get("/data", repo.ContributorsData)
+ })
+ m.Group("/code-frequency", func() {
+ m.Get("", repo.CodeFrequency)
+ m.Get("/data", repo.CodeFrequencyData)
+ })
+ m.Group("/recent-commits", func() {
+ m.Get("", repo.RecentCommits)
+ m.Get("/data", repo.RecentCommitsData)
+ })
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
m.Group("/activity_author_data", func() {
diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go
index faa35b8d2f..a87c426b3b 100644
--- a/routers/web/webfinger.go
+++ b/routers/web/webfinger.go
@@ -10,9 +10,9 @@ import (
"strings"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
// https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
diff --git a/services/actions/auth.go b/services/actions/auth.go
index 53e68f0b71..8e934d89a8 100644
--- a/services/actions/auth.go
+++ b/services/actions/auth.go
@@ -9,6 +9,7 @@ import (
"strings"
"time"
+ "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -21,24 +22,48 @@ type actionsClaims struct {
TaskID int64
RunID int64
JobID int64
+ Ac string `json:"ac"`
}
+type actionsCacheScope struct {
+ Scope string
+ Permission actionsCachePermission
+}
+
+type actionsCachePermission int
+
+const (
+ actionsCachePermissionRead = 1 << iota
+ actionsCachePermissionWrite
+)
+
func CreateAuthorizationToken(taskID, runID, jobID int64) (string, error) {
now := time.Now()
+ ac, err := json.Marshal(&[]actionsCacheScope{
+ {
+ Scope: "",
+ Permission: actionsCachePermissionWrite,
+ },
+ })
+ if err != nil {
+ return "", err
+ }
+
claims := actionsClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
NotBefore: jwt.NewNumericDate(now),
},
Scp: fmt.Sprintf("Actions.Results:%d:%d", runID, jobID),
+ Ac: string(ac),
TaskID: taskID,
RunID: runID,
JobID: jobID,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- tokenString, err := token.SignedString([]byte(setting.SecretKey))
+ tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
if err != nil {
return "", err
}
@@ -62,7 +87,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
- return []byte(setting.SecretKey), nil
+ return setting.GetGeneralTokenSigningSecret(), nil
})
if err != nil {
return 0, err
diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go
index f6288ccd5a..f73ae8ae4c 100644
--- a/services/actions/auth_test.go
+++ b/services/actions/auth_test.go
@@ -7,6 +7,7 @@ import (
"net/http"
"testing"
+ "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"github.com/golang-jwt/jwt/v5"
@@ -20,7 +21,7 @@ func TestCreateAuthorizationToken(t *testing.T) {
assert.NotEqual(t, "", token)
claims := jwt.MapClaims{}
_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
- return []byte(setting.SecretKey), nil
+ return setting.GetGeneralTokenSigningSecret(), nil
})
assert.Nil(t, err)
scp, ok := claims["scp"]
@@ -29,6 +30,14 @@ func TestCreateAuthorizationToken(t *testing.T) {
taskIDClaim, ok := claims["TaskID"]
assert.True(t, ok, "Has TaskID claim in jwt token")
assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one")
+ acClaim, ok := claims["ac"]
+ assert.True(t, ok, "Has ac claim in jwt token")
+ ac, ok := acClaim.(string)
+ assert.True(t, ok, "ac claim is a string for buildx gha cache")
+ scopes := []actionsCacheScope{}
+ err = json.Unmarshal([]byte(ac), &scopes)
+ assert.NoError(t, err, "ac claim is a json list for buildx gha cache")
+ assert.GreaterOrEqual(t, len(scopes), 1, "Expected at least one action cache scope for buildx gha cache")
}
func TestParseAuthorizationToken(t *testing.T) {
diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go
index 785eeb5838..5376c2624c 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -20,23 +20,59 @@ func Cleanup(taskCtx context.Context, olderThan time.Duration) error {
return CleanupArtifacts(taskCtx)
}
-// CleanupArtifacts removes expired artifacts and set records expired status
+// CleanupArtifacts removes expired add need-deleted artifacts and set records expired status
func CleanupArtifacts(taskCtx context.Context) error {
+ if err := cleanExpiredArtifacts(taskCtx); err != nil {
+ return err
+ }
+ return cleanNeedDeleteArtifacts(taskCtx)
+}
+
+func cleanExpiredArtifacts(taskCtx context.Context) error {
artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx)
if err != nil {
return err
}
log.Info("Found %d expired artifacts", len(artifacts))
for _, artifact := range artifacts {
- if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
- log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
- continue
- }
if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil {
log.Error("Cannot set artifact %d expired: %v", artifact.ID, err)
continue
}
+ if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
+ log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
+ continue
+ }
log.Info("Artifact %d set expired", artifact.ID)
}
return nil
}
+
+// deleteArtifactBatchSize is the batch size of deleting artifacts
+const deleteArtifactBatchSize = 100
+
+func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
+ for {
+ artifacts, err := actions.ListPendingDeleteArtifacts(taskCtx, deleteArtifactBatchSize)
+ if err != nil {
+ return err
+ }
+ log.Info("Found %d artifacts pending deletion", len(artifacts))
+ for _, artifact := range artifacts {
+ if err := actions.SetArtifactDeleted(taskCtx, artifact.ID); err != nil {
+ log.Error("Cannot set artifact %d deleted: %v", artifact.ID, err)
+ continue
+ }
+ if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
+ log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
+ continue
+ }
+ log.Info("Artifact %d set deleted", artifact.ID)
+ }
+ if len(artifacts) < deleteArtifactBatchSize {
+ log.Debug("No more artifacts pending deletion")
+ break
+ }
+ }
+ return nil
+}
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index 72a3ab7ac6..eb031511f6 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
+ commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
"github.com/nektos/act/pkg/jobparser"
)
@@ -64,6 +65,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
return fmt.Errorf("head of pull request is missing in event payload")
}
sha = payload.PullRequest.Head.Sha
+ case webhook_module.HookEventRelease:
+ event = string(run.Event)
+ sha = run.CommitSHA
default:
return nil
}
@@ -76,7 +80,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
}
ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
state := toCommitStatus(job.Status)
- if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true}); err == nil {
+ if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil {
for _, v := range statuses {
if v.Context == ctxname {
if v.State == state {
@@ -119,18 +123,13 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
if err != nil {
return fmt.Errorf("HashTypeInterfaceFromHashString: %w", err)
}
- if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
- Repo: repo,
- SHA: commitID,
- Creator: creator,
- CommitStatus: &git_model.CommitStatus{
- SHA: sha,
- TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
- Description: description,
- Context: ctxname,
- CreatorID: creator.ID,
- State: state,
- },
+ if err := commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &git_model.CommitStatus{
+ SHA: sha,
+ TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), index),
+ Description: description,
+ Context: ctxname,
+ CreatorID: creator.ID,
+ State: state,
}); err != nil {
return fmt.Errorf("NewCommitStatus: %w", err)
}
diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index fe39312386..d2bbbd9a7c 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -7,12 +7,14 @@ import (
"context"
"errors"
"fmt"
+ "strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/queue"
+ "github.com/nektos/act/pkg/jobparser"
"xorm.io/builder"
)
@@ -76,12 +78,15 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
type jobStatusResolver struct {
statuses map[int64]actions_model.Status
needs map[int64][]int64
+ jobMap map[int64]*actions_model.ActionRunJob
}
func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
idToJobs := make(map[string][]*actions_model.ActionRunJob, len(jobs))
+ jobMap := make(map[int64]*actions_model.ActionRunJob)
for _, job := range jobs {
idToJobs[job.JobID] = append(idToJobs[job.JobID], job)
+ jobMap[job.ID] = job
}
statuses := make(map[int64]actions_model.Status, len(jobs))
@@ -97,6 +102,7 @@ func newJobStatusResolver(jobs actions_model.ActionJobList) *jobStatusResolver {
return &jobStatusResolver{
statuses: statuses,
needs: needs,
+ jobMap: jobMap,
}
}
@@ -135,7 +141,20 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
if allSucceed {
ret[id] = actions_model.StatusWaiting
} else {
- ret[id] = actions_model.StatusSkipped
+ // If a job's "if" condition is "always()", the job should always run even if some of its dependencies did not succeed.
+ // See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds
+ always := false
+ if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
+ _, wfJob := wfJobs[0].Job()
+ expr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(wfJob.If.Value, "${{"), "}}"))
+ always = expr == "always()"
+ }
+
+ if always {
+ ret[id] = actions_model.StatusWaiting
+ } else {
+ ret[id] = actions_model.StatusSkipped
+ }
}
}
}
diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go
index e81aa61d80..038df7d4f8 100644
--- a/services/actions/job_emitter_test.go
+++ b/services/actions/job_emitter_test.go
@@ -70,6 +70,62 @@ func Test_jobStatusResolver_Resolve(t *testing.T) {
},
want: map[int64]actions_model.Status{},
},
+ {
+ name: "with ${{ always() }} condition",
+ jobs: actions_model.ActionJobList{
+ {ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+ {ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+ `
+name: test
+on: push
+jobs:
+ job2:
+ runs-on: ubuntu-latest
+ needs: job1
+ if: ${{ always() }}
+ steps:
+ - run: echo "always run"
+`)},
+ },
+ want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
+ },
+ {
+ name: "with always() condition",
+ jobs: actions_model.ActionJobList{
+ {ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+ {ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+ `
+name: test
+on: push
+jobs:
+ job2:
+ runs-on: ubuntu-latest
+ needs: job1
+ if: always()
+ steps:
+ - run: echo "always run"
+`)},
+ },
+ want: map[int64]actions_model.Status{2: actions_model.StatusWaiting},
+ },
+ {
+ name: "without always() condition",
+ jobs: actions_model.ActionJobList{
+ {ID: 1, JobID: "job1", Status: actions_model.StatusFailure, Needs: []string{}},
+ {ID: 2, JobID: "job2", Status: actions_model.StatusBlocked, Needs: []string{"job1"}, WorkflowPayload: []byte(
+ `
+name: test
+on: push
+jobs:
+ job2:
+ runs-on: ubuntu-latest
+ needs: job1
+ steps:
+ - run: echo "not always run"
+`)},
+ },
+ want: map[int64]actions_model.Status{2: actions_model.StatusSkipped},
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 39dfbff283..d7602b79ae 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -49,12 +49,53 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).WithPayload(&api.IssuePayload{
Action: api.HookIssueOpened,
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, issue.Poster, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, issue.Poster, nil),
}).Notify(withMethod(ctx, "NewIssue"))
}
+// IssueChangeContent notifies change content of issue
+func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
+ ctx = withMethod(ctx, "IssueChangeContent")
+
+ var err error
+ if err = issue.LoadRepo(ctx); err != nil {
+ log.Error("LoadRepo: %v", err)
+ return
+ }
+
+ permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
+ if issue.IsPull {
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ log.Error("loadPullRequest: %v", err)
+ return
+ }
+ newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequest).
+ WithDoer(doer).
+ WithPayload(&api.PullRequestPayload{
+ Action: api.HookIssueEdited,
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
+ Sender: convert.ToUser(ctx, doer, nil),
+ }).
+ WithPullRequest(issue.PullRequest).
+ Notify(ctx)
+ return
+ }
+ newNotifyInputFromIssue(issue, webhook_module.HookEventIssues).
+ WithDoer(doer).
+ WithPayload(&api.IssuePayload{
+ Action: api.HookIssueEdited,
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, permission),
+ Sender: convert.ToUser(ctx, doer, nil),
+ }).
+ Notify(ctx)
+}
+
// IssueChangeStatus notifies close or reopen issue to notifiers
func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) {
ctx = withMethod(ctx, "IssueChangeStatus")
@@ -86,7 +127,7 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
}
apiIssue := &api.IssuePayload{
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
}
@@ -101,11 +142,58 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
Notify(ctx)
}
+// IssueChangeAssignee notifies assigned or unassigned to notifiers
+func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
+ ctx = withMethod(ctx, "IssueChangeAssignee")
+
+ var action api.HookIssueAction
+ if removed {
+ action = api.HookIssueUnassigned
+ } else {
+ action = api.HookIssueAssigned
+ }
+
+ hookEvent := webhook_module.HookEventIssueAssign
+ if issue.IsPull {
+ hookEvent = webhook_module.HookEventPullRequestAssign
+ }
+
+ notifyIssueChange(ctx, doer, issue, hookEvent, action)
+}
+
+// IssueChangeMilestone notifies assignee to notifiers
+func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
+ ctx = withMethod(ctx, "IssueChangeMilestone")
+
+ var action api.HookIssueAction
+ if issue.MilestoneID > 0 {
+ action = api.HookIssueMilestoned
+ } else {
+ action = api.HookIssueDemilestoned
+ }
+
+ hookEvent := webhook_module.HookEventIssueMilestone
+ if issue.IsPull {
+ hookEvent = webhook_module.HookEventPullRequestMilestone
+ }
+
+ notifyIssueChange(ctx, doer, issue, hookEvent, action)
+}
+
func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
_, _ []*issues_model.Label,
) {
ctx = withMethod(ctx, "IssueChangeLabels")
+ hookEvent := webhook_module.HookEventIssueLabel
+ if issue.IsPull {
+ hookEvent = webhook_module.HookEventPullRequestLabel
+ }
+
+ notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated)
+}
+
+func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {
var err error
if err = issue.LoadRepo(ctx); err != nil {
log.Error("LoadRepo: %v", err)
@@ -117,20 +205,15 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
return
}
- permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
if issue.IsPull {
if err = issue.LoadPullRequest(ctx); err != nil {
log.Error("loadPullRequest: %v", err)
return
}
- if err = issue.PullRequest.LoadIssue(ctx); err != nil {
- log.Error("LoadIssue: %v", err)
- return
- }
- newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestLabel).
+ newNotifyInputFromIssue(issue, event).
WithDoer(doer).
WithPayload(&api.PullRequestPayload{
- Action: api.HookIssueLabelUpdated,
+ Action: action,
Index: issue.Index,
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
@@ -140,12 +223,13 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
Notify(ctx)
return
}
- newNotifyInputFromIssue(issue, webhook_module.HookEventIssueLabel).
+ permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
+ newNotifyInputFromIssue(issue, event).
WithDoer(doer).
WithPayload(&api.IssuePayload{
- Action: api.HookIssueLabelUpdated,
+ Action: action,
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
}).
@@ -158,37 +242,88 @@ func (n *actionsNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
) {
ctx = withMethod(ctx, "CreateIssueComment")
- permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer)
-
if issue.IsPull {
- if err := issue.LoadPullRequest(ctx); err != nil {
+ notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentCreated)
+ return
+ }
+ notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentCreated)
+}
+
+func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
+ ctx = withMethod(ctx, "UpdateComment")
+
+ if err := c.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+
+ if c.Issue.IsPull {
+ notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited)
+ return
+ }
+ notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventIssueComment, api.HookIssueCommentEdited)
+}
+
+func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) {
+ ctx = withMethod(ctx, "DeleteComment")
+
+ if err := comment.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+
+ if comment.Issue.IsPull {
+ notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted)
+ return
+ }
+ notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventIssueComment, api.HookIssueCommentDeleted)
+}
+
+func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) {
+ if err := comment.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+ if err := comment.Issue.LoadAttributes(ctx); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ return
+ }
+
+ permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer)
+
+ payload := &api.IssueCommentPayload{
+ Action: action,
+ Issue: convert.ToAPIIssue(ctx, doer, comment.Issue),
+ Comment: convert.ToAPIComment(ctx, comment.Issue.Repo, comment),
+ Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission),
+ Sender: convert.ToUser(ctx, doer, nil),
+ IsPull: comment.Issue.IsPull,
+ }
+
+ if action == api.HookIssueCommentEdited {
+ payload.Changes = &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ }
+ }
+
+ if comment.Issue.IsPull {
+ if err := comment.Issue.LoadPullRequest(ctx); err != nil {
log.Error("LoadPullRequest: %v", err)
return
}
- newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestComment).
+ newNotifyInputFromIssue(comment.Issue, event).
WithDoer(doer).
- WithPayload(&api.IssueCommentPayload{
- Action: api.HookIssueCommentCreated,
- Issue: convert.ToAPIIssue(ctx, issue),
- Comment: convert.ToAPIComment(ctx, repo, comment),
- Repository: convert.ToRepo(ctx, repo, permission),
- Sender: convert.ToUser(ctx, doer, nil),
- IsPull: true,
- }).
- WithPullRequest(issue.PullRequest).
+ WithPayload(payload).
+ WithPullRequest(comment.Issue.PullRequest).
Notify(ctx)
return
}
- newNotifyInputFromIssue(issue, webhook_module.HookEventIssueComment).
+
+ newNotifyInputFromIssue(comment.Issue, event).
WithDoer(doer).
- WithPayload(&api.IssueCommentPayload{
- Action: api.HookIssueCommentCreated,
- Issue: convert.ToAPIIssue(ctx, issue),
- Comment: convert.ToAPIComment(ctx, repo, comment),
- Repository: convert.ToRepo(ctx, repo, permission),
- Sender: convert.ToUser(ctx, doer, nil),
- IsPull: false,
- }).
+ WithPayload(payload).
Notify(ctx)
}
@@ -305,6 +440,39 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode
}).Notify(ctx)
}
+func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
+ if !issue.IsPull {
+ log.Warn("PullRequestReviewRequest: issue is not a pull request: %v", issue.ID)
+ return
+ }
+
+ ctx = withMethod(ctx, "PullRequestReviewRequest")
+
+ permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+ if err := issue.LoadPullRequest(ctx); err != nil {
+ log.Error("LoadPullRequest failed: %v", err)
+ return
+ }
+ var action api.HookIssueAction
+ if isRequest {
+ action = api.HookIssueReviewRequested
+ } else {
+ action = api.HookIssueReviewRequestRemoved
+ }
+ newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestReviewRequest).
+ WithDoer(doer).
+ WithPayload(&api.PullRequestPayload{
+ Action: action,
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ RequestedReviewer: convert.ToUser(ctx, reviewer, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, permission),
+ Sender: convert.ToUser(ctx, doer, nil),
+ }).
+ WithPullRequest(issue.PullRequest).
+ Notify(ctx)
+}
+
func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
ctx = withMethod(ctx, "MergePullRequest")
@@ -347,6 +515,12 @@ func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.U
}
func (n *actionsNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
+ commitID, _ := git.NewIDFromString(opts.NewCommitID)
+ if commitID.IsZero() {
+ log.Trace("new commitID is empty")
+ return
+ }
+
ctx = withMethod(ctx, "PushCommits")
apiPusher := convert.ToUser(ctx, pusher, nil)
@@ -379,9 +553,9 @@ func (n *actionsNotifier) CreateRef(ctx context.Context, pusher *user_model.User
apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone})
newNotifyInput(repo, pusher, webhook_module.HookEventCreate).
- WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name
+ WithRef(refFullName.String()).
WithPayload(&api.CreatePayload{
- Ref: refFullName.ShortName(),
+ Ref: refFullName.String(),
Sha: refID,
RefType: refFullName.RefType(),
Repo: apiRepo,
@@ -397,9 +571,8 @@ func (n *actionsNotifier) DeleteRef(ctx context.Context, pusher *user_model.User
apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone})
newNotifyInput(repo, pusher, webhook_module.HookEventDelete).
- WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name
WithPayload(&api.DeletePayload{
- Ref: refFullName.ShortName(),
+ Ref: refFullName.String(),
RefType: refFullName.RefType(),
PusherType: api.PusherTypeUser,
Repo: apiRepo,
@@ -456,6 +629,10 @@ func (n *actionsNotifier) UpdateRelease(ctx context.Context, doer *user_model.Us
}
func (n *actionsNotifier) DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
+ if rel.IsTag {
+ // has sent same action in `PushCommits`, so skip it.
+ return
+ }
ctx = withMethod(ctx, "DeleteRelease")
notifyRelease(ctx, doer, rel, api.HookReleaseDeleted)
}
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 77173e58a3..c48886a824 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -117,6 +117,9 @@ func notify(ctx context.Context, input *notifyInput) error {
log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
return nil
}
+ if input.Repo.IsEmpty || input.Repo.IsArchived {
+ return nil
+ }
if unit_model.TypeActions.UnitGlobalDisabled() {
if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
@@ -136,12 +139,15 @@ func notify(ctx context.Context, input *notifyInput) error {
defer gitRepo.Close()
ref := input.Ref
- if input.Event == webhook_module.HookEventDelete {
- // The event is deleting a reference, so it will fail to get the commit for a deleted reference.
- // Set ref to empty string to fall back to the default branch.
- ref = ""
+ if ref != input.Repo.DefaultBranch && actions_module.IsDefaultBranchWorkflow(input.Event) {
+ if ref != "" {
+ log.Warn("Event %q should only trigger workflows on the default branch, but its ref is %q. Will fall back to the default branch",
+ input.Event, ref)
+ }
+ ref = input.Repo.DefaultBranch
}
if ref == "" {
+ log.Warn("Ref of event %q is empty, will fall back to the default branch", input.Event)
ref = input.Repo.DefaultBranch
}
@@ -151,16 +157,17 @@ func notify(ctx context.Context, input *notifyInput) error {
return fmt.Errorf("gitRepo.GetCommit: %w", err)
}
- if skipWorkflowsForCommit(input, commit) {
+ if skipWorkflows(input, commit) {
return nil
}
var detectedWorkflows []*actions_module.DetectedWorkflow
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
+ shouldDetectSchedules := input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
input.Event,
input.Payload,
- input.Event == webhook_module.HookEventPush && git.RefName(input.Ref).BranchName() == input.Repo.DefaultBranch,
+ shouldDetectSchedules,
)
if err != nil {
return fmt.Errorf("DetectWorkflows: %w", err)
@@ -207,15 +214,17 @@ func notify(ctx context.Context, input *notifyInput) error {
}
}
- if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
- return err
+ if shouldDetectSchedules {
+ if err := handleSchedules(ctx, schedules, commit, input, ref); err != nil {
+ return err
+ }
}
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
}
-func skipWorkflowsForCommit(input *notifyInput, commit *git.Commit) bool {
- // skip workflow runs with a configured skip-ci string in commit message if the event is push or pull_request(_sync)
+func skipWorkflows(input *notifyInput, commit *git.Commit) bool {
+ // skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync)
// https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs
skipWorkflowEvents := []webhook_module.HookEventType{
webhook_module.HookEventPush,
@@ -224,6 +233,10 @@ func skipWorkflowsForCommit(input *notifyInput, commit *git.Commit) bool {
}
if slices.Contains(skipWorkflowEvents, input.Event) {
for _, s := range setting.Actions.SkipWorkflowStrings {
+ if input.PullRequest != nil && strings.Contains(input.PullRequest.Issue.Title, s) {
+ log.Debug("repo %s: skipped run for pr %v because of %s string", input.Repo.RepoPath(), input.PullRequest.Issue.ID, s)
+ return true
+ }
if strings.Contains(commit.CommitMessage, s) {
log.Debug("repo %s with commit %s: skipped run because of %s string", input.Repo.RepoPath(), commit.ID, s)
return true
@@ -287,23 +300,34 @@ func handleWorkflows(
run.NeedApproval = need
}
- jobs, err := jobparser.Parse(dwf.Content)
+ if err := run.LoadAttributes(ctx); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ continue
+ }
+
+ vars, err := actions_model.GetVariablesOfRun(ctx, run)
+ if err != nil {
+ log.Error("GetVariablesOfRun: %v", err)
+ continue
+ }
+
+ jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars))
if err != nil {
log.Error("jobparser.Parse: %v", err)
continue
}
- // cancel running jobs if the event is push
- if run.Event == webhook_module.HookEventPush {
- // cancel running jobs of the same workflow
- if err := actions_model.CancelRunningJobs(
+ // cancel running jobs if the event is push or pull_request_sync
+ if run.Event == webhook_module.HookEventPush ||
+ run.Event == webhook_module.HookEventPullRequestSync {
+ if err := actions_model.CancelPreviousJobs(
ctx,
run.RepoID,
run.Ref,
run.WorkflowID,
run.Event,
); err != nil {
- log.Error("CancelRunningJobs: %v", err)
+ log.Error("CancelPreviousJobs: %v", err)
}
}
@@ -477,6 +501,10 @@ func handleSchedules(
// DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
+ if repo.IsEmpty || repo.IsArchived {
+ return nil
+ }
+
gitRepo, err := gitrepo.OpenRepository(context.Background(), repo)
if err != nil {
return fmt.Errorf("git.OpenRepository: %w", err)
@@ -497,12 +525,9 @@ func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository)
}
// We need a notifyInput to call handleSchedules
- // Here we use the commit author as the Doer of the notifyInput
- commitUser, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
- if err != nil {
- return fmt.Errorf("get user by email: %w", err)
- }
- notifyInput := newNotifyInput(repo, commitUser, webhook_module.HookEventSchedule)
+ // if repo is a mirror, commit author maybe an external user,
+ // so we use action user as the Doer of the notifyInput
+ notifyInput := newNotifyInput(repo, user_model.NewActionsUser(), webhook_module.HookEventSchedule)
return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
}
diff --git a/services/actions/rerun.go b/services/actions/rerun.go
new file mode 100644
index 0000000000..60f6650905
--- /dev/null
+++ b/services/actions/rerun.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/container"
+)
+
+// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
+func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
+ rerunJobs := []*actions_model.ActionRunJob{job}
+ rerunJobsIDSet := make(container.Set[string])
+ rerunJobsIDSet.Add(job.JobID)
+
+ for {
+ found := false
+ for _, j := range allJobs {
+ if rerunJobsIDSet.Contains(j.JobID) {
+ continue
+ }
+ for _, need := range j.Needs {
+ if rerunJobsIDSet.Contains(need) {
+ found = true
+ rerunJobs = append(rerunJobs, j)
+ rerunJobsIDSet.Add(j.JobID)
+ break
+ }
+ }
+ }
+ if !found {
+ break
+ }
+ }
+
+ return rerunJobs
+}
diff --git a/services/actions/rerun_test.go b/services/actions/rerun_test.go
new file mode 100644
index 0000000000..a98de7b788
--- /dev/null
+++ b/services/actions/rerun_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "testing"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetAllRerunJobs(t *testing.T) {
+ job1 := &actions_model.ActionRunJob{JobID: "job1"}
+ job2 := &actions_model.ActionRunJob{JobID: "job2", Needs: []string{"job1"}}
+ job3 := &actions_model.ActionRunJob{JobID: "job3", Needs: []string{"job2"}}
+ job4 := &actions_model.ActionRunJob{JobID: "job4", Needs: []string{"job2", "job3"}}
+
+ jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
+
+ testCases := []struct {
+ job *actions_model.ActionRunJob
+ rerunJobs []*actions_model.ActionRunJob
+ }{
+ {
+ job1,
+ []*actions_model.ActionRunJob{job1, job2, job3, job4},
+ },
+ {
+ job2,
+ []*actions_model.ActionRunJob{job2, job3, job4},
+ },
+ {
+ job3,
+ []*actions_model.ActionRunJob{job3, job4},
+ },
+ {
+ job4,
+ []*actions_model.ActionRunJob{job4},
+ },
+ }
+
+ for _, tc := range testCases {
+ rerunJobs := GetAllRerunJobs(tc.job, jobs)
+ assert.ElementsMatch(t, tc.rerunJobs, rerunJobs)
+ }
+}
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 79dd84e0cc..e4e56e5122 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -55,17 +55,22 @@ func startTasks(ctx context.Context) error {
// cancel running jobs if the event is push
if row.Schedule.Event == webhook_module.HookEventPush {
// cancel running jobs of the same workflow
- if err := actions_model.CancelRunningJobs(
+ if err := actions_model.CancelPreviousJobs(
ctx,
row.RepoID,
row.Schedule.Ref,
row.Schedule.WorkflowID,
webhook_module.HookEventSchedule,
); err != nil {
- log.Error("CancelRunningJobs: %v", err)
+ log.Error("CancelPreviousJobs: %v", err)
}
}
+ if row.Repo.IsArchived {
+ // Skip if the repo is archived
+ continue
+ }
+
cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
if err != nil {
if repo_model.IsErrUnitTypeNotExist(err) {
diff --git a/services/actions/variables.go b/services/actions/variables.go
new file mode 100644
index 0000000000..8dde9c4af5
--- /dev/null
+++ b/services/actions/variables.go
@@ -0,0 +1,100 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "context"
+ "regexp"
+ "strings"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
+ secret_service "code.gitea.io/gitea/services/secrets"
+)
+
+func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
+ if err := secret_service.ValidateName(name); err != nil {
+ return nil, err
+ }
+
+ if err := envNameCIRegexMatch(name); err != nil {
+ return nil, err
+ }
+
+ v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data))
+ if err != nil {
+ return nil, err
+ }
+
+ return v, nil
+}
+
+func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
+ if err := secret_service.ValidateName(name); err != nil {
+ return false, err
+ }
+
+ if err := envNameCIRegexMatch(name); err != nil {
+ return false, err
+ }
+
+ return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
+ ID: variableID,
+ Name: strings.ToUpper(name),
+ Data: util.ReserveLineBreakForTextarea(data),
+ })
+}
+
+func DeleteVariableByID(ctx context.Context, variableID int64) error {
+ return actions_model.DeleteVariable(ctx, variableID)
+}
+
+func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
+ if err := secret_service.ValidateName(name); err != nil {
+ return err
+ }
+
+ if err := envNameCIRegexMatch(name); err != nil {
+ return err
+ }
+
+ v, err := GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ownerID,
+ RepoID: repoID,
+ Name: name,
+ })
+ if err != nil {
+ return err
+ }
+
+ return actions_model.DeleteVariable(ctx, v.ID)
+}
+
+func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) {
+ vars, err := actions_model.FindVariables(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ if len(vars) != 1 {
+ return nil, util.NewNotExistErrorf("variable not found")
+ }
+ return vars[0], nil
+}
+
+// some regular expression of `variables` and `secrets`
+// reference to:
+// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
+// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
+var (
+ forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
+)
+
+func envNameCIRegexMatch(name string) error {
+ if forbiddenEnvNameCIRx.MatchString(name) {
+ log.Error("Env Name cannot be ci")
+ return util.NewInvalidArgumentErrorf("env name cannot be ci")
+ }
+ return nil
+}
diff --git a/services/agit/agit.go b/services/agit/agit.go
index bc68372570..52a70469e0 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"os"
+ "strconv"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
@@ -15,28 +16,25 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
+ "code.gitea.io/gitea/modules/setting"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
)
// ProcReceive handle proc receive work
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
- // TODO: Add more options?
- var (
- topicBranch string
- title string
- description string
- forcePush bool
- )
-
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
+ topicBranch := opts.GitPushOptions["topic"]
+ forcePush, _ := strconv.ParseBool(opts.GitPushOptions["force-push"])
+ title := strings.TrimSpace(opts.GitPushOptions["title"])
+ description := strings.TrimSpace(opts.GitPushOptions["description"]) // TODO: Add more options?
+ objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+ userName := strings.ToLower(opts.UserName)
- ownerName := repo.OwnerName
- repoName := repo.Name
-
- topicBranch = opts.GitPushOptions["topic"]
- _, forcePush = opts.GitPushOptions["force-push"]
- objectFormat, _ := gitRepo.GetObjectFormat()
+ pusher, err := user_model.GetUserByID(ctx, opts.UserID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get user. Error: %w", err)
+ }
for i := range opts.OldCommitIDs {
if opts.NewCommitIDs[i] == objectFormat.EmptyObjectID().String() {
@@ -80,9 +78,6 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
continue
}
- var headBranch string
- userName := strings.ToLower(opts.UserName)
-
if len(curentTopicBranch) == 0 {
curentTopicBranch = topicBranch
}
@@ -90,6 +85,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
// because different user maybe want to use same topic,
// So it's better to make sure the topic branch name
// has user name prefix
+ var headBranch string
if !strings.HasPrefix(curentTopicBranch, userName+"/") {
headBranch = userName + "/" + curentTopicBranch
} else {
@@ -99,26 +95,26 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
- return nil, fmt.Errorf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %w", ownerName, repoName, err)
+ return nil, fmt.Errorf("failed to get unmerged agit flow pull request in repository: %s Error: %w", repo.FullName(), err)
+ }
+
+ var commit *git.Commit
+ if title == "" || description == "" {
+ commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i])
+ if err != nil {
+ return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err)
+ }
}
// create a new pull request
- if len(title) == 0 {
- var has bool
- title, has = opts.GitPushOptions["title"]
- if !has || len(title) == 0 {
- commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i])
- if err != nil {
- return nil, fmt.Errorf("Failed to get commit %s in repository: %s/%s Error: %w", opts.NewCommitIDs[i], ownerName, repoName, err)
- }
- title = strings.Split(commit.CommitMessage, "\n")[0]
- }
- description = opts.GitPushOptions["description"]
+ if title == "" {
+ title = strings.Split(commit.CommitMessage, "\n")[0]
}
-
- pusher, err := user_model.GetUserByID(ctx, opts.UserID)
- if err != nil {
- return nil, fmt.Errorf("Failed to get user. Error: %w", err)
+ if description == "" {
+ _, description, _ = strings.Cut(commit.CommitMessage, "\n\n")
+ }
+ if description == "" {
+ description = title
}
prIssue := &issues_model.Issue{
@@ -149,24 +145,27 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
- objectFormat, _ := gitRepo.GetObjectFormat()
results = append(results, private.HookProcReceiveRefResult{
- Ref: pr.GetGitRefName(),
- OriginalRef: opts.RefFullNames[i],
- OldOID: objectFormat.EmptyObjectID().String(),
- NewOID: opts.NewCommitIDs[i],
+ Ref: pr.GetGitRefName(),
+ OriginalRef: opts.RefFullNames[i],
+ OldOID: objectFormat.EmptyObjectID().String(),
+ NewOID: opts.NewCommitIDs[i],
+ IsCreatePR: true,
+ URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
+ ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
+ HeadBranch: headBranch,
})
continue
}
// update exist pull request
if err := pr.LoadBaseRepo(ctx); err != nil {
- return nil, fmt.Errorf("Unable to load base repository for PR[%d] Error: %w", pr.ID, err)
+ return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err)
}
oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
- return nil, fmt.Errorf("Unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
+ return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
}
if oldCommitID == opts.NewCommitIDs[i] {
@@ -180,9 +179,11 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
}
if !forcePush {
- output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
+ output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").
+ AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).
+ RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
if err != nil {
- return nil, fmt.Errorf("Fail to detect force push: %w", err)
+ return nil, fmt.Errorf("failed to detect force push: %w", err)
} else if len(output) > 0 {
results = append(results, private.HookProcReceiveRefResult{
OriginalRef: opts.RefFullNames[i],
@@ -196,17 +197,13 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
pr.HeadCommitID = opts.NewCommitIDs[i]
if err = pull_service.UpdateRef(ctx, pr); err != nil {
- return nil, fmt.Errorf("Failed to update pull ref. Error: %w", err)
+ return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
}
pull_service.AddToTaskQueue(ctx, pr)
- pusher, err := user_model.GetUserByID(ctx, opts.UserID)
- if err != nil {
- return nil, fmt.Errorf("Failed to get user. Error: %w", err)
- }
err = pr.LoadIssue(ctx)
if err != nil {
- return nil, fmt.Errorf("Failed to load pull issue. Error: %w", err)
+ return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)
}
comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
if err == nil && comment != nil {
@@ -216,11 +213,14 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
isForcePush := comment != nil && comment.IsForcePush
results = append(results, private.HookProcReceiveRefResult{
- OldOID: oldCommitID,
- NewOID: opts.NewCommitIDs[i],
- Ref: pr.GetGitRefName(),
- OriginalRef: opts.RefFullNames[i],
- IsForcePush: isForcePush,
+ OldOID: oldCommitID,
+ NewOID: opts.NewCommitIDs[i],
+ Ref: pr.GetGitRefName(),
+ OriginalRef: opts.RefFullNames[i],
+ IsForcePush: isForcePush,
+ IsCreatePR: false,
+ URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
+ ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
})
}
diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go
index e127cbfc6e..324688c534 100644
--- a/services/asymkey/deploy_key.go
+++ b/services/asymkey/deploy_key.go
@@ -7,7 +7,6 @@ import (
"context"
"code.gitea.io/gitea/models"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
)
@@ -27,5 +26,5 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error
return err
}
- return asymkey_model.RewriteAllPublicKeys(ctx)
+ return RewriteAllPublicKeys(ctx)
}
diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go
index 83d7edafa3..da57059d4b 100644
--- a/services/asymkey/ssh_key.go
+++ b/services/asymkey/ssh_key.go
@@ -43,8 +43,8 @@ func DeletePublicKey(ctx context.Context, doer *user_model.User, id int64) (err
committer.Close()
if key.Type == asymkey_model.KeyTypePrincipal {
- return asymkey_model.RewriteAllPrincipalKeys(ctx)
+ return RewriteAllPrincipalKeys(ctx)
}
- return asymkey_model.RewriteAllPublicKeys(ctx)
+ return RewriteAllPublicKeys(ctx)
}
diff --git a/services/asymkey/ssh_key_authorized_keys.go b/services/asymkey/ssh_key_authorized_keys.go
new file mode 100644
index 0000000000..5caa5bbfb6
--- /dev/null
+++ b/services/asymkey/ssh_key_authorized_keys.go
@@ -0,0 +1,79 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again.
+// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
+// outside any session scope independently.
+func RewriteAllPublicKeys(ctx context.Context) error {
+ // Don't rewrite key if internal server
+ if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
+ return nil
+ }
+
+ return asymkey_model.WithSSHOpLocker(func() error {
+ return rewriteAllPublicKeys(ctx)
+ })
+}
+
+func rewriteAllPublicKeys(ctx context.Context) error {
+ if setting.SSH.RootPath != "" {
+ // First of ensure that the RootPath is present, and if not make it with 0700 permissions
+ // This of course doesn't guarantee that this is the right directory for authorized_keys
+ // but at least if it's supposed to be this directory and it doesn't exist and we're the
+ // right user it will at least be created properly.
+ err := os.MkdirAll(setting.SSH.RootPath, 0o700)
+ if err != nil {
+ log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
+ return err
+ }
+ }
+
+ fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+ tmpPath := fPath + ".tmp"
+ t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ t.Close()
+ if err := util.Remove(tmpPath); err != nil {
+ log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err)
+ }
+ }()
+
+ if setting.SSH.AuthorizedKeysBackup {
+ isExist, err := util.IsExist(fPath)
+ if err != nil {
+ log.Error("Unable to check if %s exists. Error: %v", fPath, err)
+ return err
+ }
+ if isExist {
+ bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
+ if err = util.CopyFile(fPath, bakPath); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err := asymkey_model.RegeneratePublicKeys(ctx, t); err != nil {
+ return err
+ }
+
+ t.Close()
+ return util.Rename(tmpPath, fPath)
+}
diff --git a/models/asymkey/ssh_key_authorized_principals.go b/services/asymkey/ssh_key_authorized_principals.go
similarity index 72%
rename from models/asymkey/ssh_key_authorized_principals.go
rename to services/asymkey/ssh_key_authorized_principals.go
index 107d70c766..2838bb5fc7 100644
--- a/models/asymkey/ssh_key_authorized_principals.go
+++ b/services/asymkey/ssh_key_authorized_principals.go
@@ -13,31 +13,22 @@ import (
"strings"
"time"
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
-// _____ __ .__ .__ .___
-// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/
-// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ |
-// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ |
-// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ |
-// \/ \/ \/ \/ \/
-// __________ .__ .__ .__
-// \______ _______|__| ____ ____ |_____________ | | ______
-// | ___\_ __ | |/ \_/ ___\| \____ \__ \ | | / ___/
-// | | | | \| | | \ \___| | |_> / __ \| |__\___ \
-// |____| |__| |__|___| /\___ |__| __(____ |____/____ >
-// \/ \/ |__| \/ \/
-//
// This file contains functions for creating authorized_principals files
//
// There is a dependence on the database within RewriteAllPrincipalKeys & RegeneratePrincipalKeys
// The sshOpLocker is used from ssh_key_authorized_keys.go
-const authorizedPrincipalsFile = "authorized_principals"
+const (
+ authorizedPrincipalsFile = "authorized_principals"
+ tplCommentPrefix = `# gitea public key`
+)
// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
// Note: db.GetEngine(ctx).Iterate does not get latest data after insert/delete, so we have to call this function
@@ -48,9 +39,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
return nil
}
- sshOpLocker.Lock()
- defer sshOpLocker.Unlock()
+ return asymkey_model.WithSSHOpLocker(func() error {
+ return rewriteAllPrincipalKeys(ctx)
+ })
+}
+func rewriteAllPrincipalKeys(ctx context.Context) error {
if setting.SSH.RootPath != "" {
// First of ensure that the RootPath is present, and if not make it with 0700 permissions
// This of course doesn't guarantee that this is the right directory for authorized_keys
@@ -97,8 +91,8 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
}
func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
- if err := db.GetEngine(ctx).Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
- _, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
+ if err := db.GetEngine(ctx).Where("type = ?", asymkey_model.KeyTypePrincipal).Iterate(new(asymkey_model.PublicKey), func(idx int, bean any) (err error) {
+ _, err = t.WriteString((bean.(*asymkey_model.PublicKey)).AuthorizedString())
return err
}); err != nil {
return err
@@ -115,6 +109,8 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
if err != nil {
return err
}
+ defer f.Close()
+
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
@@ -124,11 +120,12 @@ func regeneratePrincipalKeys(ctx context.Context, t io.StringWriter) error {
}
_, err = t.WriteString(line + "\n")
if err != nil {
- f.Close()
return err
}
}
- f.Close()
+ if err = scanner.Err(); err != nil {
+ return fmt.Errorf("regeneratePrincipalKeys scan: %w", err)
+ }
}
return nil
}
diff --git a/services/asymkey/ssh_key_principals.go b/services/asymkey/ssh_key_principals.go
new file mode 100644
index 0000000000..5ed5cfa782
--- /dev/null
+++ b/services/asymkey/ssh_key_principals.go
@@ -0,0 +1,54 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package asymkey
+
+import (
+ "context"
+ "fmt"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
+)
+
+// AddPrincipalKey adds new principal to database and authorized_principals file.
+func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*asymkey_model.PublicKey, error) {
+ dbCtx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer committer.Close()
+
+ // Principals cannot be duplicated.
+ has, err := db.GetEngine(dbCtx).
+ Where("content = ? AND type = ?", content, asymkey_model.KeyTypePrincipal).
+ Get(new(asymkey_model.PublicKey))
+ if err != nil {
+ return nil, err
+ } else if has {
+ return nil, asymkey_model.ErrKeyAlreadyExist{
+ Content: content,
+ }
+ }
+
+ key := &asymkey_model.PublicKey{
+ OwnerID: ownerID,
+ Name: content,
+ Content: content,
+ Mode: perm.AccessModeWrite,
+ Type: asymkey_model.KeyTypePrincipal,
+ LoginSourceID: authSourceID,
+ }
+ if err = db.Insert(dbCtx, key); err != nil {
+ return nil, fmt.Errorf("addKey: %w", err)
+ }
+
+ if err = committer.Commit(); err != nil {
+ return nil, err
+ }
+
+ committer.Close()
+
+ return key, RewriteAllPrincipalKeys(ctx)
+}
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index 967332fd98..0fd51e4fa5 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -12,8 +12,8 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context/upload"
"github.com/google/uuid"
)
@@ -39,14 +39,14 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
}
// UploadAttachment upload new attachment into storage and update database
-func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, opts *repo_model.Attachment) (*repo_model.Attachment, error) {
+func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(file, buf)
buf = buf[:n]
- if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil {
+ if err := upload.Verify(buf, attach.Name, allowedTypes); err != nil {
return nil, err
}
- return NewAttachment(ctx, opts, io.MultiReader(bytes.NewReader(buf), file), fileSize)
+ return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize)
}
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 6746dc2a54..a2523a2452 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -12,12 +12,12 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/webauthn"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
+ gitea_context "code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
)
@@ -40,6 +40,7 @@ func isContainerPath(req *http.Request) bool {
var (
gitRawOrAttachPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/(?:(?:git-(?:(?:upload)|(?:receive))-pack$)|(?:info/refs$)|(?:HEAD$)|(?:objects/)|(?:raw/)|(?:releases/download/)|(?:attachments/))`)
lfsPathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/info/lfs/`)
+ archivePathRe = regexp.MustCompile(`^/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+/archive/`)
)
func isGitRawOrAttachPath(req *http.Request) bool {
@@ -56,6 +57,10 @@ func isGitRawOrAttachOrLFSPath(req *http.Request) bool {
return false
}
+func isArchivePath(req *http.Request) bool {
+ return archivePathRe.MatchString(req.URL.Path)
+}
+
// handleSignIn clears existing session variables and stores new ones for the specified user object
func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) {
// We need to regenerate the session...
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index f2f7858a85..46d8510143 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -133,7 +133,7 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
- !isGitRawOrAttachPath(req) {
+ !isGitRawOrAttachPath(req) && !isArchivePath(req) {
return nil, nil
}
diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go
index 359c1f2473..b6aeb0aed2 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -10,8 +10,8 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
gouuid "github.com/google/uuid"
@@ -161,7 +161,7 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
}
overwriteDefault := user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
}
if err := user_model.CreateUser(req.Context(), user, &overwriteDefault); err != nil {
diff --git a/services/auth/session.go b/services/auth/session.go
index d13813dcbe..35d97e42da 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -4,7 +4,6 @@
package auth
import (
- "context"
"net/http"
user_model "code.gitea.io/gitea/models/user"
@@ -29,40 +28,33 @@ func (s *Session) Name() string {
// object for that uid.
// Returns nil if there is no user uid stored in the session.
func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
- user := SessionUser(req.Context(), sess)
- if user != nil {
- return user, nil
- }
- return nil, nil
-}
-
-// SessionUser returns the user object corresponding to the "uid" session variable.
-func SessionUser(ctx context.Context, sess SessionStore) *user_model.User {
if sess == nil {
- return nil
+ return nil, nil
}
// Get user ID
uid := sess.Get("uid")
if uid == nil {
- return nil
+ return nil, nil
}
log.Trace("Session Authorization: Found user[%d]", uid)
id, ok := uid.(int64)
if !ok {
- return nil
+ return nil, nil
}
// Get user object
- user, err := user_model.GetUserByID(ctx, id)
+ user, err := user_model.GetUserByID(req.Context(), id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
- log.Error("GetUserById: %v", err)
+ log.Error("GetUserByID: %v", err)
+ // Return the err as-is to keep current signed-in session, in case the err is something like context.Canceled. Otherwise non-existing user (nil, nil) will make the caller clear the signed-in session.
+ return nil, err
}
- return nil
+ return nil, nil
}
log.Trace("Session Authorization: Logged in user %-v", user)
- return user
+ return user, nil
}
diff --git a/services/auth/signin.go b/services/auth/signin.go
index fafe3ef3c6..e116a088e0 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/auth/source/smtp"
@@ -87,7 +87,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use
}
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
})
if err != nil {
return nil, nil, err
diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go
index 50eae27439..bb2270cbd6 100644
--- a/services/auth/source/db/source.go
+++ b/services/auth/source/db/source.go
@@ -18,7 +18,7 @@ func (source *Source) FromDB(bs []byte) error {
return nil
}
-// ToDB exports an SMTPConfig to a serialized format.
+// ToDB exports the config to a byte slice to be saved into database (this method is just dummy and does nothing for DB source)
func (source *Source) ToDB() ([]byte, error) {
return nil, nil
}
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 8f641ed541..6ebd3ea50a 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -13,7 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/util"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user"
)
@@ -69,7 +69,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
if user != nil {
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sr.SSHPublicKey) {
- if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+ if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err
}
}
@@ -85,8 +85,8 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
IsAdmin: sr.IsAdmin,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
- IsActive: util.OptionalBoolTrue,
+ IsRestricted: optional.Some(sr.IsRestricted),
+ IsActive: optional.Some(true),
}
err := user_model.CreateUser(ctx, user, overwriteDefault)
@@ -95,7 +95,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
}
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(ctx, user, source.authSource, sr.SSHPublicKey) {
- if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+ if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return user, err
}
}
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index eee7bb585a..0c9491cd09 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -16,7 +16,7 @@ import (
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/util"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
source_service "code.gitea.io/gitea/services/auth/source"
user_service "code.gitea.io/gitea/services/user"
)
@@ -78,7 +78,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.authSource.Name)
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
if sshKeysNeedUpdate {
- err = asymkey_model.RewriteAllPublicKeys(ctx)
+ err = asymkey_service.RewriteAllPublicKeys(ctx)
if err != nil {
log.Error("RewriteAllPublicKeys: %v", err)
}
@@ -125,8 +125,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
IsAdmin: su.IsAdmin,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsRestricted: util.OptionalBoolOf(su.IsRestricted),
- IsActive: util.OptionalBoolTrue,
+ IsRestricted: optional.Some(su.IsRestricted),
+ IsActive: optional.Some(true),
}
err = user_model.CreateUser(ctx, usr, overwriteDefault)
@@ -196,7 +196,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
if sshKeysNeedUpdate {
- err = asymkey_model.RewriteAllPublicKeys(ctx)
+ err = asymkey_service.RewriteAllPublicKeys(ctx)
if err != nil {
log.Error("RewriteAllPublicKeys: %v", err)
}
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
index 3ad6e307f1..5c25681548 100644
--- a/services/auth/source/oauth2/init.go
+++ b/services/auth/source/oauth2/init.go
@@ -12,8 +12,8 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"github.com/google/uuid"
"github.com/gorilla/sessions"
@@ -66,7 +66,7 @@ func ResetOAuth2(ctx context.Context) error {
// initOAuth2Sources is used to load and register all active OAuth2 providers
func initOAuth2Sources(ctx context.Context) error {
authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
LoginType: auth.OAuth2,
})
if err != nil {
diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
index eca0b8b7e1..070fffe60f 100644
--- a/services/auth/source/oauth2/jwtsigningkey.go
+++ b/services/auth/source/oauth2/jwtsigningkey.go
@@ -300,7 +300,7 @@ func InitSigningKey() error {
case "HS384":
fallthrough
case "HS512":
- key, err = loadSymmetricKey()
+ key = setting.GetGeneralTokenSigningSecret()
case "RS256":
fallthrough
case "RS384":
@@ -333,12 +333,6 @@ func InitSigningKey() error {
return nil
}
-// loadSymmetricKey checks if the configured secret is valid.
-// If it is not valid, it will return an error.
-func loadSymmetricKey() (any, error) {
- return util.Base64FixedDecode(base64.RawURLEncoding, []byte(setting.OAuth2.JWTSecretBase64), 32)
-}
-
// loadOrCreateAsymmetricKey checks if the configured private key exists.
// If it does not exist a new random key gets generated and saved on the configured path.
func loadOrCreateAsymmetricKey() (any, error) {
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index f4edb507f2..6ed6c184eb 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -15,8 +15,8 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"github.com/markbates/goth"
)
@@ -59,7 +59,7 @@ func (p *AuthSourceProvider) DisplayName() string {
func (p *AuthSourceProvider) IconHTML(size int) template.HTML {
if p.iconURL != "" {
- img := fmt.Sprintf(`
`,
+ img := fmt.Sprintf(`
`,
size,
size,
html.EscapeString(p.iconURL), html.EscapeString(p.DisplayName()),
@@ -107,7 +107,7 @@ func CreateProviderFromSource(source *auth.Source) (Provider, error) {
}
// GetOAuth2Providers returns the list of configured OAuth2 providers
-func GetOAuth2Providers(ctx context.Context, isActive util.OptionalBool) ([]Provider, error) {
+func GetOAuth2Providers(ctx context.Context, isActive optional.Option[bool]) ([]Provider, error) {
authSources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
IsActive: isActive,
LoginType: auth.OAuth2,
diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go
index 5b6694487b..9d4ab106e5 100644
--- a/services/auth/source/oauth2/providers_base.go
+++ b/services/auth/source/oauth2/providers_base.go
@@ -35,10 +35,10 @@ func (b *BaseProvider) IconHTML(size int) template.HTML {
case "github":
svgName = "octicon-mark-github"
}
- svgHTML := svg.RenderHTML(svgName, size, "gt-mr-3")
+ svgHTML := svg.RenderHTML(svgName, size, "tw-mr-2")
if svgHTML == "" {
log.Error("No SVG icon for oauth2 provider %q", b.name)
- svgHTML = svg.RenderHTML("gitea-openid", size, "gt-mr-3")
+ svgHTML = svg.RenderHTML("gitea-openid", size, "tw-mr-2")
}
return svgHTML
}
diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go
index a4dcfcafc7..285876d5ac 100644
--- a/services/auth/source/oauth2/providers_openid.go
+++ b/services/auth/source/oauth2/providers_openid.go
@@ -29,7 +29,7 @@ func (o *OpenIDProvider) DisplayName() string {
// IconHTML returns icon HTML for this provider
func (o *OpenIDProvider) IconHTML(size int) template.HTML {
- return svg.RenderHTML("gitea-openid", size, "gt-mr-3")
+ return svg.RenderHTML("gitea-openid", size, "tw-mr-2")
}
// CreateGothProvider creates a GothProvider from this Provider
diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go
index 0891a86392..addd1bd2c9 100644
--- a/services/auth/source/pam/source_authenticate.go
+++ b/services/auth/source/pam/source_authenticate.go
@@ -11,8 +11,8 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/pam"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"github.com/google/uuid"
)
@@ -60,7 +60,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
LoginName: userName, // This is what the user typed in
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
}
if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go
index b244fc7d40..1f0a61c789 100644
--- a/services/auth/source/smtp/source_authenticate.go
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -12,6 +12,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util"
)
@@ -75,7 +76,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
LoginName: userName,
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
}
if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
index 3a2411ec55..05293f202f 100644
--- a/services/auth/source/source_group_sync.go
+++ b/services/auth/source/source_group_sync.go
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
}
if action == syncAdd && !isMember {
- if err := models.AddTeamMember(ctx, team, user.ID); err != nil {
+ if err := models.AddTeamMember(ctx, team, user); err != nil {
log.Error("group sync: Could not add user to team: %v", err)
return err
}
} else if action == syncRemove && isMember {
- if err := models.RemoveTeamMember(ctx, team, user.ID); err != nil {
+ if err := models.RemoveTeamMember(ctx, team, user); err != nil {
log.Error("group sync: Could not remove user from team: %v", err)
return err
}
diff --git a/services/auth/sspi.go b/services/auth/sspi.go
index 0e974fde8f..64a127e97a 100644
--- a/services/auth/sspi.go
+++ b/services/auth/sspi.go
@@ -14,12 +14,12 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/auth/source/sspi"
+ gitea_context "code.gitea.io/gitea/services/context"
gouuid "github.com/google/uuid"
)
@@ -131,7 +131,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
// getConfig retrieves the SSPI configuration from login sources
func (s *SSPI) getConfig(ctx context.Context) (*sspi.Source, error) {
sources, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
LoginType: auth.SSPI,
})
if err != nil {
@@ -172,8 +172,8 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) (
}
emailNotificationPreference := user_model.EmailNotificationsDisabled
overwriteDefault := &user_model.CreateUserOverwriteOptions{
- IsActive: util.OptionalBoolOf(cfg.AutoActivateUsers),
- KeepEmailPrivate: util.OptionalBoolTrue,
+ IsActive: optional.Some(cfg.AutoActivateUsers),
+ KeepEmailPrivate: optional.Some(true),
EmailNotificationsPreference: &emailNotificationPreference,
}
if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil {
diff --git a/modules/context/access_log.go b/services/context/access_log.go
similarity index 100%
rename from modules/context/access_log.go
rename to services/context/access_log.go
diff --git a/modules/context/api.go b/services/context/api.go
similarity index 97%
rename from modules/context/api.go
rename to services/context/api.go
index e226264a87..b18a206b5e 100644
--- a/modules/context/api.go
+++ b/services/context/api.go
@@ -245,7 +245,7 @@ func APIContexter() func(http.Handler) http.Handler {
// NotFound handles 404s for APIContext
// String will replace message, errors will be added to a slice
func (ctx *APIContext) NotFound(objs ...any) {
- message := ctx.Tr("error.not_found")
+ message := ctx.Locale.TrString("error.not_found")
var errors []string
for _, obj := range objs {
// Ignore nil
@@ -307,12 +307,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
return
}
- objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetCommit", err)
- return
- }
-
if ref := ctx.FormTrim("ref"); len(ref) > 0 {
commit, err := ctx.Repo.GitRepo.GetCommit(ref)
if err != nil {
@@ -331,6 +325,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
}
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
+ var err error
if ctx.Repo.GitRepo.IsBranchExist(refName) {
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
@@ -346,7 +341,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
return
}
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
- } else if len(refName) == objectFormat.FullLength() {
+ } else if len(refName) == ctx.Repo.GetObjectFormat().FullLength() {
ctx.Repo.CommitID = refName
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
if err != nil {
diff --git a/modules/context/api_org.go b/services/context/api_org.go
similarity index 100%
rename from modules/context/api_org.go
rename to services/context/api_org.go
diff --git a/modules/context/api_test.go b/services/context/api_test.go
similarity index 100%
rename from modules/context/api_test.go
rename to services/context/api_test.go
diff --git a/modules/context/base.go b/services/context/base.go
similarity index 89%
rename from modules/context/base.go
rename to services/context/base.go
index 8df1dde866..62fb743714 100644
--- a/modules/context/base.go
+++ b/services/context/base.go
@@ -6,6 +6,7 @@ package context
import (
"context"
"fmt"
+ "html/template"
"io"
"net/http"
"net/url"
@@ -16,8 +17,8 @@ import (
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"github.com/go-chi/chi/v5"
@@ -206,17 +207,17 @@ func (b *Base) FormBool(key string) bool {
return v
}
-// FormOptionalBool returns an OptionalBoolTrue or OptionalBoolFalse if the value
-// for the provided key exists in the form else it returns OptionalBoolNone
-func (b *Base) FormOptionalBool(key string) util.OptionalBool {
+// FormOptionalBool returns an optional.Some(true) or optional.Some(false) if the value
+// for the provided key exists in the form else it returns optional.None[bool]()
+func (b *Base) FormOptionalBool(key string) optional.Option[bool] {
value := b.Req.FormValue(key)
if len(value) == 0 {
- return util.OptionalBoolNone
+ return optional.None[bool]()
}
s := b.Req.FormValue(key)
v, _ := strconv.ParseBool(s)
v = v || strings.EqualFold(s, "on")
- return util.OptionalBoolOf(v)
+ return optional.Some(v)
}
func (b *Base) SetFormString(key, value string) {
@@ -255,7 +256,7 @@ func (b *Base) Redirect(location string, status ...int) {
code = status[0]
}
- if strings.Contains(location, "://") || strings.HasPrefix(location, "//") {
+ if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") || strings.HasPrefix(location, "//") {
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
// 1. the first request to "/my-path" contains cookie
// 2. some time later, the request to "/my-path" doesn't contain cookie (caused by Prevent web tracking)
@@ -264,6 +265,14 @@ func (b *Base) Redirect(location string, status ...int) {
// So in this case, we should remove the session cookie from the response header
removeSessionCookieHeader(b.Resp)
}
+ // in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx
+ if b.Req.Header.Get("HX-Request") == "true" {
+ b.Resp.Header().Set("HX-Redirect", location)
+ // we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect
+ // so as to give htmx redirect logic a chance to run
+ b.Status(http.StatusNoContent)
+ return
+ }
http.Redirect(b.Resp, b.Req, location, code)
}
@@ -286,11 +295,11 @@ func (b *Base) cleanUp() {
}
}
-func (b *Base) Tr(msg string, args ...any) string {
+func (b *Base) Tr(msg string, args ...any) template.HTML {
return b.Locale.Tr(msg, args...)
}
-func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string {
+func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return b.Locale.TrN(cnt, key1, keyN, args...)
}
diff --git a/services/context/base_test.go b/services/context/base_test.go
new file mode 100644
index 0000000000..823f20e00b
--- /dev/null
+++ b/services/context/base_test.go
@@ -0,0 +1,47 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRedirect(t *testing.T) {
+ req, _ := http.NewRequest("GET", "/", nil)
+
+ cases := []struct {
+ url string
+ keep bool
+ }{
+ {"http://test", false},
+ {"https://test", false},
+ {"//test", false},
+ {"/://test", true},
+ {"/test", true},
+ }
+ for _, c := range cases {
+ resp := httptest.NewRecorder()
+ b, cleanup := NewBaseContext(resp, req)
+ resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String())
+ b.Redirect(c.url)
+ cleanup()
+ has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
+ assert.Equal(t, c.keep, has, "url = %q", c.url)
+ }
+
+ req, _ = http.NewRequest("GET", "/", nil)
+ resp := httptest.NewRecorder()
+ req.Header.Add("HX-Request", "true")
+ b, cleanup := NewBaseContext(resp, req)
+ b.Redirect("/other")
+ cleanup()
+ assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
+ assert.Equal(t, http.StatusNoContent, resp.Code)
+}
diff --git a/modules/context/captcha.go b/services/context/captcha.go
similarity index 95%
rename from modules/context/captcha.go
rename to services/context/captcha.go
index a1999900c9..fa8d779f56 100644
--- a/modules/context/captcha.go
+++ b/services/context/captcha.go
@@ -79,11 +79,11 @@ func VerifyCaptcha(ctx *Context, tpl base.TplName, form any) {
case setting.CfTurnstile:
valid, err = turnstile.Verify(ctx, ctx.Req.Form.Get(cfTurnstileResponseField))
default:
- ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
+ ctx.ServerError("Unknown Captcha Type", fmt.Errorf("unknown Captcha Type: %s", setting.Service.CaptchaType))
return
}
if err != nil {
- log.Debug("%v", err)
+ log.Debug("Captcha Verify failed: %v", err)
}
if !valid {
diff --git a/modules/context/context.go b/services/context/context.go
similarity index 93%
rename from modules/context/context.go
rename to services/context/context.go
index d19c5d1198..4b318f7e33 100644
--- a/modules/context/context.go
+++ b/services/context/context.go
@@ -6,7 +6,8 @@ package context
import (
"context"
- "html"
+ "encoding/hex"
+ "fmt"
"html/template"
"io"
"net/http"
@@ -71,16 +72,6 @@ func init() {
})
}
-// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
-// This is useful if the locale message is intended to only produce HTML content.
-func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
- trArgs := make([]any, len(args))
- for i, arg := range args {
- trArgs[i] = html.EscapeString(arg)
- }
- return ctx.Locale.Tr(msg, trArgs...)
-}
-
type webContextKeyType struct{}
var WebContextKey = webContextKeyType{}
@@ -134,7 +125,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
csrfOpts := CsrfOptions{
- Secret: setting.SecretKey,
+ Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
Cookie: setting.CSRFCookieName,
SetCookie: true,
Secure: setting.SessionConfig.Secure,
@@ -201,6 +192,7 @@ func Contexter() func(next http.Handler) http.Handler {
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
+ ctx.Data["SystemConfig"] = setting.Config()
ctx.Data["CsrfToken"] = ctx.Csrf.GetToken()
ctx.Data["CsrfTokenHtml"] = template.HTML(``)
@@ -253,6 +245,13 @@ func (ctx *Context) JSONOK() {
ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
}
-func (ctx *Context) JSONError(msg string) {
- ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg})
+func (ctx *Context) JSONError(msg any) {
+ switch v := msg.(type) {
+ case string:
+ ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
+ case template.HTML:
+ ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
+ default:
+ panic(fmt.Sprintf("unsupported type: %T", msg))
+ }
}
diff --git a/modules/context/context_cookie.go b/services/context/context_cookie.go
similarity index 100%
rename from modules/context/context_cookie.go
rename to services/context/context_cookie.go
diff --git a/modules/context/context_model.go b/services/context/context_model.go
similarity index 100%
rename from modules/context/context_model.go
rename to services/context/context_model.go
diff --git a/modules/context/context_request.go b/services/context/context_request.go
similarity index 100%
rename from modules/context/context_request.go
rename to services/context/context_request.go
diff --git a/modules/context/context_response.go b/services/context/context_response.go
similarity index 80%
rename from modules/context/context_response.go
rename to services/context/context_response.go
index 5729865561..d7fd18acac 100644
--- a/modules/context/context_response.go
+++ b/services/context/context_response.go
@@ -6,6 +6,7 @@ package context
import (
"errors"
"fmt"
+ "html/template"
"net"
"net/http"
"net/url"
@@ -43,14 +44,14 @@ func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
}
-// RedirectToFirst redirects to first not empty URL
-func (ctx *Context) RedirectToFirst(location ...string) {
+// RedirectToCurrentSite redirects to first not empty URL which belongs to current site
+func (ctx *Context) RedirectToCurrentSite(location ...string) {
for _, loc := range location {
if len(loc) == 0 {
continue
}
- if httplib.IsRiskyRedirectURL(loc) {
+ if !httplib.IsCurrentGiteaSiteURL(loc) {
continue
}
@@ -90,20 +91,33 @@ func (ctx *Context) HTML(status int, name base.TplName) {
}
}
-// RenderToString renders the template content to a string
-func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) {
+// JSONTemplate renders the template as JSON response
+// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
+func (ctx *Context) JSONTemplate(tmpl base.TplName) {
+ t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
+ if err != nil {
+ ctx.ServerError("unable to find template", err)
+ return
+ }
+ ctx.Resp.Header().Set("Content-Type", "application/json")
+ if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
+ ctx.ServerError("unable to execute template", err)
+ }
+}
+
+// RenderToHTML renders the template content to a HTML string
+func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) {
var buf strings.Builder
- err := ctx.Render.HTML(&buf, http.StatusOK, string(name), data, ctx.TemplateContext)
- return buf.String(), err
+ err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext)
+ return template.HTML(buf.String()), err
}
// RenderWithErr used for page has form validation but need to prompt error to users.
-func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form any) {
+func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
if form != nil {
middleware.AssignForm(form, ctx.Data)
}
- ctx.Flash.ErrorMsg = msg
- ctx.Data["Flash"] = ctx.Flash
+ ctx.Flash.Error(msg, true)
ctx.HTML(http.StatusOK, tpl)
}
diff --git a/modules/context/context_template.go b/services/context/context_template.go
similarity index 53%
rename from modules/context/context_template.go
rename to services/context/context_template.go
index ba90fc170a..7878d409ca 100644
--- a/modules/context/context_template.go
+++ b/services/context/context_template.go
@@ -5,10 +5,7 @@ package context
import (
"context"
- "errors"
"time"
-
- "code.gitea.io/gitea/modules/log"
)
var _ context.Context = TemplateContext(nil)
@@ -36,14 +33,3 @@ func (c TemplateContext) Err() error {
func (c TemplateContext) Value(key any) any {
return c.parentContext().Value(key)
}
-
-// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context
-// as the current template's rendering context (request context), to help to find data race issues as early as possible.
-// When the code is proven to be correct and stable, this function should be removed.
-func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) {
- if c.parentContext() != dataCtx {
- log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2))
- return "", errors.New("parent context mismatch")
- }
- return "", nil
-}
diff --git a/services/context/context_test.go b/services/context/context_test.go
new file mode 100644
index 0000000000..984593398d
--- /dev/null
+++ b/services/context/context_test.go
@@ -0,0 +1,51 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRemoveSessionCookieHeader(t *testing.T) {
+ w := httptest.NewRecorder()
+ w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
+ w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
+ assert.Len(t, w.Header().Values("Set-Cookie"), 2)
+ removeSessionCookieHeader(w)
+ assert.Len(t, w.Header().Values("Set-Cookie"), 1)
+ assert.Contains(t, "other=bar", w.Header().Get("Set-Cookie"))
+}
+
+func TestRedirectToCurrentSite(t *testing.T) {
+ defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
+ defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+ cases := []struct {
+ location string
+ want string
+ }{
+ {"/", "/sub/"},
+ {"http://localhost:3000/sub?k=v", "http://localhost:3000/sub?k=v"},
+ {"http://other", "/sub/"},
+ }
+ for _, c := range cases {
+ t.Run(c.location, func(t *testing.T) {
+ req := &http.Request{URL: &url.URL{Path: "/"}}
+ resp := httptest.NewRecorder()
+ base, baseCleanUp := NewBaseContext(resp, req)
+ defer baseCleanUp()
+ ctx := NewWebContext(base, nil, nil)
+ ctx.RedirectToCurrentSite(c.location)
+ redirect := test.RedirectURL(resp)
+ assert.Equal(t, c.want, redirect)
+ })
+ }
+}
diff --git a/modules/context/csrf.go b/services/context/csrf.go
similarity index 100%
rename from modules/context/csrf.go
rename to services/context/csrf.go
diff --git a/modules/context/org.go b/services/context/org.go
similarity index 93%
rename from modules/context/org.go
rename to services/context/org.go
index d068646577..018b76de43 100644
--- a/modules/context/org.go
+++ b/services/context/org.go
@@ -11,6 +11,8 @@ import (
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
)
@@ -255,6 +257,19 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
+
+ ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+ if len(ctx.ContextUser.Description) != 0 {
+ content, err := markdown.RenderString(&markup.RenderContext{
+ Metas: map[string]string{"mode": "document"},
+ Ctx: ctx,
+ }, ctx.ContextUser.Description)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+ ctx.Data["RenderedDescription"] = content
+ }
}
// OrgAssignment returns a middleware to handle organization assignment
diff --git a/modules/context/package.go b/services/context/package.go
similarity index 100%
rename from modules/context/package.go
rename to services/context/package.go
diff --git a/modules/context/pagination.go b/services/context/pagination.go
similarity index 70%
rename from modules/context/pagination.go
rename to services/context/pagination.go
index 68237c630c..fb2ef699ce 100644
--- a/modules/context/pagination.go
+++ b/services/context/pagination.go
@@ -26,17 +26,6 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination {
return p
}
-// AddParam adds a value from context identified by ctxKey as link param under a given paramKey
-func (p *Pagination) AddParam(ctx *Context, paramKey, ctxKey string) {
- _, exists := ctx.Data[ctxKey]
- if !exists {
- return
- }
- paramData := fmt.Sprintf("%v", ctx.Data[ctxKey]) // cast any to string
- urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(paramKey), url.QueryEscape(paramData))
- p.urlParams = append(p.urlParams, urlParam)
-}
-
// AddParamString adds a string parameter directly
func (p *Pagination) AddParamString(key, value string) {
urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value))
@@ -50,8 +39,14 @@ func (p *Pagination) GetParams() template.URL {
// SetDefaultParams sets common pagination params that are often used
func (p *Pagination) SetDefaultParams(ctx *Context) {
- p.AddParam(ctx, "sort", "SortType")
- p.AddParam(ctx, "q", "Keyword")
+ if v, ok := ctx.Data["SortType"].(string); ok {
+ p.AddParamString("sort", v)
+ }
+ if v, ok := ctx.Data["Keyword"].(string); ok {
+ p.AddParamString("q", v)
+ }
+ if v, ok := ctx.Data["IsFuzzy"].(bool); ok {
+ p.AddParamString("fuzzy", fmt.Sprint(v))
+ }
// do not add any more uncommon params here!
- p.AddParam(ctx, "t", "queryType")
}
diff --git a/modules/context/permission.go b/services/context/permission.go
similarity index 100%
rename from modules/context/permission.go
rename to services/context/permission.go
diff --git a/modules/context/private.go b/services/context/private.go
similarity index 100%
rename from modules/context/private.go
rename to services/context/private.go
diff --git a/modules/context/repo.go b/services/context/repo.go
similarity index 96%
rename from modules/context/repo.go
rename to services/context/repo.go
index 75ebfec705..56e9fada0e 100644
--- a/modules/context/repo.go
+++ b/services/context/repo.go
@@ -6,6 +6,7 @@ package context
import (
"context"
+ "errors"
"fmt"
"html"
"net/http"
@@ -26,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -81,11 +83,15 @@ func (r *Repository) CanCreateBranch() bool {
return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
}
+func (r *Repository) GetObjectFormat() git.ObjectFormat {
+ return git.ObjectFormatFromName(r.Repository.ObjectFormatName)
+}
+
// RepoMustNotBeArchived checks if a repo is archived
func RepoMustNotBeArchived() func(ctx *Context) {
return func(ctx *Context) {
if ctx.Repo.Repository.IsArchived {
- ctx.NotFound("IsArchived", fmt.Errorf(ctx.Tr("repo.archive.title")))
+ ctx.NotFound("IsArchived", errors.New(ctx.Locale.TrString("repo.archive.title")))
}
}
}
@@ -402,26 +408,6 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
}
-// RepoIDAssignment returns a handler which assigns the repo to the context.
-func RepoIDAssignment() func(ctx *Context) {
- return func(ctx *Context) {
- repoID := ctx.ParamsInt64(":repoid")
-
- // Get repository.
- repo, err := repo_model.GetRepositoryByID(ctx, repoID)
- if err != nil {
- if repo_model.IsErrRepoNotExist(err) {
- ctx.NotFound("GetRepositoryByID", nil)
- } else {
- ctx.ServerError("GetRepositoryByID", err)
- }
- return
- }
-
- repoAssignment(ctx, repo)
- }
-}
-
// RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) context.CancelFunc {
if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce {
@@ -540,7 +526,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
ctx.Data["NumTags"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
IncludeDrafts: true,
IncludeTags: true,
- HasSha1: util.OptionalBoolTrue, // only draft releases which are created with existing tags
+ HasSha1: optional.Some(true), // only draft releases which are created with existing tags
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@@ -670,7 +656,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
- IsDeletedBranch: util.OptionalBoolFalse,
+ IsDeletedBranch: optional.Some(false),
ListOptions: db.ListOptionsAll,
}
branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
@@ -695,7 +681,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) {
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
} else {
- ctx.Repo.BranchName, _ = gitRepo.GetDefaultBranch()
+ ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository)
if ctx.Repo.BranchName == "" {
// If it still can't get a default branch, fall back to default branch from setting.
// Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug.
@@ -828,9 +814,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
}
// For legacy and API support only full commit sha
parts := strings.Split(path, "/")
- objectFormat, _ := repo.GitRepo.GetObjectFormat()
- if len(parts) > 0 && len(parts[0]) == objectFormat.FullLength() {
+ if len(parts) > 0 && len(parts[0]) == git.ObjectFormatFromName(repo.Repository.ObjectFormatName).FullLength() {
repo.TreePath = strings.Join(parts[1:], "/")
return parts[0]
}
@@ -874,9 +859,8 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
case RepoRefCommit:
parts := strings.Split(path, "/")
- objectFormat, _ := repo.GitRepo.GetObjectFormat()
- if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= objectFormat.FullLength() {
+ if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= repo.GetObjectFormat().FullLength() {
repo.TreePath = strings.Join(parts[1:], "/")
return parts[0]
}
@@ -935,12 +919,6 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
}
}
- objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
- if err != nil {
- log.Error("Cannot determine objectFormat for repository: %w", err)
- ctx.Repo.Repository.MarkAsBrokenEmpty()
- }
-
// Get default branch.
if len(ctx.Params("*")) == 0 {
refName = ctx.Repo.Repository.DefaultBranch
@@ -1007,7 +985,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
return cancel
}
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
- } else if len(refName) >= 7 && len(refName) <= objectFormat.FullLength() {
+ } else if len(refName) >= 7 && len(refName) <= ctx.Repo.GetObjectFormat().FullLength() {
ctx.Repo.IsViewCommit = true
ctx.Repo.CommitID = refName
@@ -1017,7 +995,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
return cancel
}
// If short commit ID add canonical link header
- if len(refName) < objectFormat.FullLength() {
+ if len(refName) < ctx.Repo.GetObjectFormat().FullLength() {
ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
}
diff --git a/modules/context/response.go b/services/context/response.go
similarity index 100%
rename from modules/context/response.go
rename to services/context/response.go
diff --git a/modules/upload/upload.go b/services/context/upload/upload.go
similarity index 98%
rename from modules/upload/upload.go
rename to services/context/upload/upload.go
index cd10715864..77a7eb9377 100644
--- a/modules/upload/upload.go
+++ b/services/context/upload/upload.go
@@ -11,9 +11,9 @@ import (
"regexp"
"strings"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/context"
)
// ErrFileTypeForbidden not allowed file type error
diff --git a/modules/upload/upload_test.go b/services/context/upload/upload_test.go
similarity index 100%
rename from modules/upload/upload_test.go
rename to services/context/upload/upload_test.go
diff --git a/services/context/user.go b/services/context/user.go
index 8b2faf3369..4c9cd2928b 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -9,12 +9,11 @@ import (
"strings"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
)
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
-func UserAssignmentWeb() func(ctx *context.Context) {
- return func(ctx *context.Context) {
+func UserAssignmentWeb() func(ctx *Context) {
+ return func(ctx *Context) {
errorFn := func(status int, title string, obj any) {
err, ok := obj.(error)
if !ok {
@@ -32,8 +31,8 @@ func UserAssignmentWeb() func(ctx *context.Context) {
}
// UserIDAssignmentAPI returns a middleware to handle context-user assignment for api routes
-func UserIDAssignmentAPI() func(ctx *context.APIContext) {
- return func(ctx *context.APIContext) {
+func UserIDAssignmentAPI() func(ctx *APIContext) {
+ return func(ctx *APIContext) {
userID := ctx.ParamsInt64(":user-id")
if ctx.IsSigned && ctx.Doer.ID == userID {
@@ -53,13 +52,13 @@ func UserIDAssignmentAPI() func(ctx *context.APIContext) {
}
// UserAssignmentAPI returns a middleware to handle context-user assignment for api routes
-func UserAssignmentAPI() func(ctx *context.APIContext) {
- return func(ctx *context.APIContext) {
+func UserAssignmentAPI() func(ctx *APIContext) {
+ return func(ctx *APIContext) {
ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, ctx.Error)
}
}
-func userAssignment(ctx *context.Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) {
+func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, any)) (contextUser *user_model.User) {
username := ctx.Params(":username")
if doer != nil && doer.LowerName == strings.ToLower(username) {
@@ -70,7 +69,7 @@ func userAssignment(ctx *context.Base, doer *user_model.User, errCb func(int, st
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
- context.RedirectToUser(ctx, username, redirectUserID)
+ RedirectToUser(ctx, username, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
errCb(http.StatusNotFound, "GetUserByName", err)
} else {
diff --git a/modules/context/utils.go b/services/context/utils.go
similarity index 100%
rename from modules/context/utils.go
rename to services/context/utils.go
diff --git a/modules/context/xsrf.go b/services/context/xsrf.go
similarity index 100%
rename from modules/context/xsrf.go
rename to services/context/xsrf.go
diff --git a/modules/context/xsrf_test.go b/services/context/xsrf_test.go
similarity index 100%
rename from modules/context/xsrf_test.go
rename to services/context/xsrf_test.go
diff --git a/modules/contexttest/context_tests.go b/services/contexttest/context_tests.go
similarity index 94%
rename from modules/contexttest/context_tests.go
rename to services/contexttest/context_tests.go
index c9bacf259f..3064c56590 100644
--- a/modules/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -7,21 +7,23 @@ package contexttest
import (
gocontext "context"
"io"
+ "maps"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
+ "time"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
@@ -35,7 +37,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
}
requestURL, err := url.Parse(path)
assert.NoError(t, err)
- req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}}
+ req := &http.Request{Method: method, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}}
req = req.WithContext(middleware.WithContextData(req.Context()))
return req
}
@@ -61,7 +63,9 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
base.Locale = &translation.MockLocale{}
ctx := context.NewWebContext(base, opt.Render, nil)
-
+ ctx.AppendContextValue(context.WebContextKey, ctx)
+ ctx.PageData = map[string]any{}
+ ctx.Data["PageStartTime"] = time.Now()
chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp
diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go
index ed08691c8b..e0efcddbcb 100644
--- a/services/convert/git_commit.go
+++ b/services/convert/git_commit.go
@@ -10,11 +10,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- ctx "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ ctx "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
)
diff --git a/services/convert/issue.go b/services/convert/issue.go
index c6e06180c8..54b00cd88e 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -18,19 +18,19 @@ import (
api "code.gitea.io/gitea/modules/structs"
)
-func ToIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
- return toIssue(ctx, issue, WebAssetDownloadURL)
+func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+ return toIssue(ctx, doer, issue, WebAssetDownloadURL)
}
// ToAPIIssue converts an Issue to API format
// it assumes some fields assigned with values:
// Required - Poster, Labels,
// Optional - Milestone, Assignee, PullRequest
-func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
- return toIssue(ctx, issue, APIAssetDownloadURL)
+func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+ return toIssue(ctx, doer, issue, APIAssetDownloadURL)
}
-func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
+func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
if err := issue.LoadLabels(ctx); err != nil {
return &api.Issue{}
}
@@ -44,7 +44,7 @@ func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func
apiIssue := &api.Issue{
ID: issue.ID,
Index: issue.Index,
- Poster: ToUser(ctx, issue.Poster, nil),
+ Poster: ToUser(ctx, issue.Poster, doer),
Title: issue.Title,
Body: issue.Content,
Attachments: toAttachments(issue.Repo, issue.Attachments, getDownloadURL),
@@ -114,25 +114,25 @@ func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func
}
// ToIssueList converts an IssueList to API format
-func ToIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+func ToIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
result := make([]*api.Issue, len(il))
for i := range il {
- result[i] = ToIssue(ctx, il[i])
+ result[i] = ToIssue(ctx, doer, il[i])
}
return result
}
// ToAPIIssueList converts an IssueList to API format
-func ToAPIIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+func ToAPIIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
result := make([]*api.Issue, len(il))
for i := range il {
- result[i] = ToAPIIssue(ctx, il[i])
+ result[i] = ToAPIIssue(ctx, doer, il[i])
}
return result
}
// ToTrackedTime converts TrackedTime to API format
-func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
+func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
apiT = &api.TrackedTime{
ID: t.ID,
IssueID: t.IssueID,
@@ -141,7 +141,7 @@ func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.
Created: t.Created,
}
if t.Issue != nil {
- apiT.Issue = ToAPIIssue(ctx, t.Issue)
+ apiT.Issue = ToAPIIssue(ctx, doer, t.Issue)
}
if t.User != nil {
apiT.UserName = t.User.Name
@@ -192,10 +192,10 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
}
// ToTrackedTimeList converts TrackedTimeList to API format
-func ToTrackedTimeList(ctx context.Context, tl issues_model.TrackedTimeList) api.TrackedTimeList {
+func ToTrackedTimeList(ctx context.Context, doer *user_model.User, tl issues_model.TrackedTimeList) api.TrackedTimeList {
result := make([]*api.TrackedTime, 0, len(tl))
for _, t := range tl {
- result = append(result, ToTrackedTime(ctx, t))
+ result = append(result, ToTrackedTime(ctx, doer, t))
}
return result
}
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
index b034a50897..9ffaf1e84c 100644
--- a/services/convert/issue_comment.go
+++ b/services/convert/issue_comment.go
@@ -120,7 +120,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
return nil
}
- comment.TrackedTime = ToTrackedTime(ctx, c.Time)
+ comment.TrackedTime = ToTrackedTime(ctx, doer, c.Time)
}
if c.RefIssueID != 0 {
@@ -129,7 +129,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
return nil
}
- comment.RefIssue = ToAPIIssue(ctx, issue)
+ comment.RefIssue = ToAPIIssue(ctx, doer, issue)
}
if c.RefCommentID != 0 {
@@ -180,7 +180,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
}
if c.DependentIssue != nil {
- comment.DependentIssue = ToAPIIssue(ctx, c.DependentIssue)
+ comment.DependentIssue = ToAPIIssue(ctx, doer, c.DependentIssue)
}
return comment
diff --git a/services/convert/notification.go b/services/convert/notification.go
index 0b97530d8b..41063cf399 100644
--- a/services/convert/notification.go
+++ b/services/convert/notification.go
@@ -61,8 +61,9 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx)
}
- pr, _ := n.Issue.GetPullRequest(ctx)
- if pr != nil && pr.HasMerged {
+ if err := n.Issue.LoadPullRequest(ctx); err == nil &&
+ n.Issue.PullRequest != nil &&
+ n.Issue.PullRequest.HasMerged {
result.Subject.State = "merged"
}
}
diff --git a/services/convert/package.go b/services/convert/package.go
index e90ce8a00f..b5fca21a3c 100644
--- a/services/convert/package.go
+++ b/services/convert/package.go
@@ -35,7 +35,7 @@ func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_m
Name: pd.Package.Name,
Version: pd.Version.Version,
CreatedAt: pd.Version.CreatedUnix.AsTime(),
- HTMLURL: pd.FullWebLink(),
+ HTMLURL: pd.VersionHTMLURL(),
}, nil
}
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 6d98121ed5..775bf3806d 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -33,7 +33,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
return nil
}
- apiIssue := ToAPIIssue(ctx, pr.Issue)
+ apiIssue := ToAPIIssue(ctx, doer, pr.Issue)
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
return nil
diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go
index aa7ad68a47..29a5ab7466 100644
--- a/services/convert/pull_review.go
+++ b/services/convert/pull_review.go
@@ -66,7 +66,7 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user
result := make([]*api.PullReview, 0, len(rl))
for i := range rl {
// show pending reviews only for the user who created them
- if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) {
+ if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || (!doer.IsAdmin && doer.ID != rl[i].ReviewerID)) {
continue
}
r, err := ToPullReview(ctx, rl[i], doer)
diff --git a/services/convert/pull_review_test.go b/services/convert/pull_review_test.go
new file mode 100644
index 0000000000..6886950280
--- /dev/null
+++ b/services/convert/pull_review_test.go
@@ -0,0 +1,52 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_ToPullReview(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 6})
+ assert.EqualValues(t, reviewer.ID, review.ReviewerID)
+ assert.EqualValues(t, issues_model.ReviewTypePending, review.Type)
+
+ reviewList := []*issues_model.Review{review}
+
+ t.Run("Anonymous User", func(t *testing.T) {
+ prList, err := ToPullReviewList(db.DefaultContext, reviewList, nil)
+ assert.NoError(t, err)
+ assert.Empty(t, prList)
+ })
+
+ t.Run("Reviewer Himself", func(t *testing.T) {
+ prList, err := ToPullReviewList(db.DefaultContext, reviewList, reviewer)
+ assert.NoError(t, err)
+ assert.Len(t, prList, 1)
+ })
+
+ t.Run("Other User", func(t *testing.T) {
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ prList, err := ToPullReviewList(db.DefaultContext, reviewList, user4)
+ assert.NoError(t, err)
+ assert.Len(t, prList, 0)
+ })
+
+ t.Run("Admin User", func(t *testing.T) {
+ adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ prList, err := ToPullReviewList(db.DefaultContext, reviewList, adminUser)
+ assert.NoError(t, err)
+ assert.Len(t, prList, 1)
+ })
+}
diff --git a/services/convert/repository.go b/services/convert/repository.go
index c16180c0af..39efd304a9 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -93,6 +93,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
allowRebase := false
allowRebaseMerge := false
allowSquash := false
+ allowFastForwardOnly := false
allowRebaseUpdate := false
defaultDeleteBranchAfterMerge := false
defaultMergeStyle := repo_model.MergeStyleMerge
@@ -105,14 +106,18 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
allowRebase = config.AllowRebase
allowRebaseMerge = config.AllowRebaseMerge
allowSquash = config.AllowSquash
+ allowFastForwardOnly = config.AllowFastForwardOnly
allowRebaseUpdate = config.AllowRebaseUpdate
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
defaultMergeStyle = config.GetDefaultMergeStyle()
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
}
hasProjects := false
- if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
+ projectsMode := repo_model.ProjectsModeAll
+ if unit, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
hasProjects = true
+ config := unit.ProjectsConfig()
+ projectsMode = config.ProjectsMode
}
hasReleases := false
@@ -209,6 +214,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
InternalTracker: internalTracker,
HasWiki: hasWiki,
HasProjects: hasProjects,
+ ProjectsMode: string(projectsMode),
HasReleases: hasReleases,
HasPackages: hasPackages,
HasActions: hasActions,
@@ -219,6 +225,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
AllowRebase: allowRebase,
AllowRebaseMerge: allowRebaseMerge,
AllowSquash: allowSquash,
+ AllowFastForwardOnly: allowFastForwardOnly,
AllowRebaseUpdate: allowRebaseUpdate,
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
DefaultMergeStyle: string(defaultMergeStyle),
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 0656307cba..6dad88830a 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -70,7 +70,7 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
// Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string {
realArgs := make([]any, 0, len(args)+2)
- realArgs = append(realArgs, locale.Tr("admin.dashboard."+name))
+ realArgs = append(realArgs, locale.TrString("admin.dashboard."+name))
if doer == "" {
realArgs = append(realArgs, "(Cron)")
} else {
@@ -80,7 +80,7 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer
realArgs = append(realArgs, args...)
}
if doer == "" {
- return locale.Tr("admin.dashboard.cron."+status, realArgs...)
+ return locale.TrString("admin.dashboard.cron."+status, realArgs...)
}
- return locale.Tr("admin.dashboard.task."+status, realArgs...)
+ return locale.TrString("admin.dashboard.task."+status, realArgs...)
}
diff --git a/services/cron/tasks.go b/services/cron/tasks.go
index f0956a97d8..f8a7444c49 100644
--- a/services/cron/tasks.go
+++ b/services/cron/tasks.go
@@ -159,7 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
log.Debug("Registering task: %s", name)
i18nKey := "admin.dashboard." + name
- if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey {
+ if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey {
return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
}
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
index 1dd5d70a38..0018c5facc 100644
--- a/services/cron/tasks_extended.go
+++ b/services/cron/tasks_extended.go
@@ -8,13 +8,13 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/updatechecker"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
user_service "code.gitea.io/gitea/services/user"
@@ -71,7 +71,7 @@ func registerRewriteAllPublicKeys() {
RunAtStart: false,
Schedule: "@every 72h",
}, func(ctx context.Context, _ *user_model.User, _ Config) error {
- return asymkey_model.RewriteAllPublicKeys(ctx)
+ return asymkey_service.RewriteAllPublicKeys(ctx)
})
}
@@ -81,7 +81,7 @@ func registerRewriteAllPrincipalKeys() {
RunAtStart: false,
Schedule: "@every 72h",
}, func(ctx context.Context, _ *user_model.User, _ Config) error {
- return asymkey_model.RewriteAllPrincipalKeys(ctx)
+ return asymkey_service.RewriteAllPrincipalKeys(ctx)
})
}
diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go
index 050a4e7974..8d6fc9cb5e 100644
--- a/services/doctor/authorizedkeys.go
+++ b/services/doctor/authorizedkeys.go
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
)
const tplCommentPrefix = `# gitea public key`
@@ -33,7 +34,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
return fmt.Errorf("Unable to open authorized_keys file. ERROR: %w", err)
}
logger.Warn("Unable to open authorized_keys. (ERROR: %v). Attempting to rewrite...", err)
- if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+ if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
}
@@ -50,7 +51,11 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
}
linesInAuthorizedKeys.Add(line)
}
- f.Close()
+ if err = scanner.Err(); err != nil {
+ return fmt.Errorf("scan: %w", err)
+ }
+ // although there is a "defer close" above, here close explicitly before the generating, because it needs to open the file for writing again
+ _ = f.Close()
// now we regenerate and check if there are any lines missing
regenerated := &bytes.Buffer{}
@@ -76,7 +81,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "gitea admin regenerate keys" or "gitea doctor --run authorized-keys --fix"`)
}
logger.Warn("authorized_keys is out of date. Attempting rewrite...")
- err = asymkey_model.RewriteAllPublicKeys(ctx)
+ err = asymkey_service.RewriteAllPublicKeys(ctx)
if err != nil {
logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %w", err)
diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go
index e2dcb63f33..dfdf7b547a 100644
--- a/services/doctor/dbconsistency.go
+++ b/services/doctor/dbconsistency.go
@@ -61,26 +61,20 @@ func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int6
}
}
-func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCheck {
+func genericOrphanCheck(name, subject, refObject, joinCond string) consistencyCheck {
return consistencyCheck{
Name: name,
Counter: func(ctx context.Context) (int64, error) {
- return db.CountOrphanedObjects(ctx, subject, refobject, joincond)
+ return db.CountOrphanedObjects(ctx, subject, refObject, joinCond)
},
Fixer: func(ctx context.Context) (int64, error) {
- err := db.DeleteOrphanedObjects(ctx, subject, refobject, joincond)
+ err := db.DeleteOrphanedObjects(ctx, subject, refObject, joinCond)
return -1, err
},
}
}
-func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
- // make sure DB version is uptodate
- if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
- logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
- return err
- }
-
+func prepareDBConsistencyChecks() []consistencyCheck {
consistencyChecks := []consistencyCheck{
{
// find labels without existing repo or org
@@ -210,7 +204,7 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
"oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"),
// find OAuth2Application without existing user
genericOrphanCheck("Orphaned OAuth2Application without existing User",
- "oauth2_application", "user", "oauth2_application.uid=`user`.id"),
+ "oauth2_application", "user", "oauth2_application.uid=0 OR oauth2_application.uid=`user`.id"),
// find OAuth2AuthorizationCode without existing OAuth2Grant
genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant",
"oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"),
@@ -224,7 +218,16 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
genericOrphanCheck("Orphaned Redirects without existing redirect user",
"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
)
+ return consistencyChecks
+}
+func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
+ // make sure DB version is uptodate
+ if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
+ logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
+ return err
+ }
+ consistencyChecks := prepareDBConsistencyChecks()
for _, c := range consistencyChecks {
if err := c.Run(ctx, logger, autofix); err != nil {
return err
diff --git a/services/doctor/dbconsistency_test.go b/services/doctor/dbconsistency_test.go
new file mode 100644
index 0000000000..4e4ac535b7
--- /dev/null
+++ b/services/doctor/dbconsistency_test.go
@@ -0,0 +1,51 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+ "slices"
+ "testing"
+
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestConsistencyCheck(t *testing.T) {
+ checks := prepareDBConsistencyChecks()
+ idx := slices.IndexFunc(checks, func(check consistencyCheck) bool {
+ return check.Name == "Orphaned OAuth2Application without existing User"
+ })
+ if !assert.NotEqual(t, -1, idx) {
+ return
+ }
+
+ _ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &user.User{})
+ _ = db.TruncateBeans(db.DefaultContext, &auth.OAuth2Application{}, &auth.OAuth2Application{})
+
+ err := db.Insert(db.DefaultContext, &user.User{ID: 1})
+ assert.NoError(t, err)
+ err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-1", ClientID: "client-id-1"})
+ assert.NoError(t, err)
+ err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-2", ClientID: "client-id-2", UID: 1})
+ assert.NoError(t, err)
+ err = db.Insert(db.DefaultContext, &auth.OAuth2Application{Name: "test-oauth2-app-3", ClientID: "client-id-3", UID: 99999999})
+ assert.NoError(t, err)
+
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-1"})
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-2"})
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-3"})
+
+ oauth2AppCheck := checks[idx]
+ err = oauth2AppCheck.Run(db.DefaultContext, log.GetManager().GetLogger(log.DEFAULT), true)
+ assert.NoError(t, err)
+
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-1"})
+ unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ClientID: "client-id-2"})
+ unittest.AssertNotExistsBean(t, &auth.OAuth2Application{ClientID: "client-id-3"})
+}
diff --git a/services/doctor/doctor.go b/services/doctor/doctor.go
index 559f8e06da..a4eb5e16b9 100644
--- a/services/doctor/doctor.go
+++ b/services/doctor/doctor.go
@@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
)
// Check represents a Doctor check
@@ -25,6 +26,7 @@ type Check struct {
AbortIfFailed bool
SkipDatabaseInitialization bool
Priority int
+ InitStorage bool
}
func initDBSkipLogger(ctx context.Context) error {
@@ -84,6 +86,7 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize})
dbIsInit := false
+ storageIsInit := false
for i, check := range checks {
if !dbIsInit && !check.SkipDatabaseInitialization {
// Only open database after the most basic configuration check
@@ -94,6 +97,14 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
}
dbIsInit = true
}
+ if !storageIsInit && check.InitStorage {
+ if err := storage.Init(); err != nil {
+ logger.Error("Error whilst initializing the storage: %v", err)
+ logger.Error("Check if you are using the right config file. You can use a --config directive to specify one.")
+ return nil
+ }
+ storageIsInit = true
+ }
logger.Info("\n[%d] %s", i+1, check.Title)
if err := check.Run(ctx, loggerStep, autofix); err != nil {
if check.AbortIfFailed {
diff --git a/services/doctor/fix16961.go b/services/doctor/fix16961.go
index d3f36d8d5c..50d9ac6621 100644
--- a/services/doctor/fix16961.go
+++ b/services/doctor/fix16961.go
@@ -216,6 +216,12 @@ func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed boo
return false, nil
}
+ var cfg any
+ err = json.UnmarshalHandleDoubleEncode(bs, &cfg)
+ if err == nil {
+ return false, nil
+ }
+
switch repoUnit.Type {
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
cfg := &repo_model.UnitConfig{}
diff --git a/services/doctor/main_test.go b/services/doctor/main_test.go
new file mode 100644
index 0000000000..0f365e21d0
--- /dev/null
+++ b/services/doctor/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m)
+}
diff --git a/services/doctor/storage.go b/services/doctor/storage.go
index f338537864..787df27549 100644
--- a/services/doctor/storage.go
+++ b/services/doctor/storage.go
@@ -162,7 +162,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
if opts.RepoArchives || opts.All {
if err := commonCheckStorage(ctx, logger, autofix,
&commonStorageCheckOptions{
- storer: storage.RepoAvatars,
+ storer: storage.RepoArchives,
isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
exists, err := repo.ExistsRepoArchiverWithStoragePath(ctx, path)
if err == nil || errors.Is(err, util.ErrInvalidArgument) {
diff --git a/services/forms/admin.go b/services/forms/admin.go
index 4b3cacc606..81276f8f46 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -6,9 +6,9 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
@@ -41,6 +41,7 @@ type AdminEditUserForm struct {
Password string `binding:"MaxSize(255)"`
Website string `binding:"ValidUrl;MaxSize(255)"`
Location string `binding:"MaxSize(50)"`
+ Language string `binding:"MaxSize(5)"`
MaxRepoCreation int
Active bool
Admin bool
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 25acbbb99e..c9f3182b3a 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
diff --git a/services/forms/org.go b/services/forms/org.go
index 6e2d787516..3677fcf429 100644
--- a/services/forms/org.go
+++ b/services/forms/org.go
@@ -7,9 +7,9 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index 2f08dfe9f4..cc940d42d3 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go
index 5deb0ae463..42e6c85c37 100644
--- a/services/forms/repo_branch_form.go
+++ b/services/forms/repo_branch_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 845eccf817..e45a2a1695 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -12,10 +12,10 @@ import (
"code.gitea.io/gitea/models"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/webhook"
"gitea.com/go-chi/binding"
@@ -133,6 +133,7 @@ type RepoSettingForm struct {
EnableCode bool
EnableWiki bool
EnableExternalWiki bool
+ DefaultWikiBranch string
ExternalWikiURL string
EnableIssues bool
EnableExternalTracker bool
@@ -142,6 +143,7 @@ type RepoSettingForm struct {
ExternalTrackerRegexpPattern string
EnableCloseIssuesViaCommitInAnyBranch bool
EnableProjects bool
+ ProjectsMode string
EnableReleases bool
EnablePackages bool
EnablePulls bool
@@ -151,6 +153,7 @@ type RepoSettingForm struct {
PullsAllowRebase bool
PullsAllowRebaseMerge bool
PullsAllowSquash bool
+ PullsAllowFastForwardOnly bool
PullsAllowManualMerge bool
PullsDefaultMergeStyle string
EnableAutodetectManualMerge bool
@@ -313,7 +316,7 @@ func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) bind
errs = append(errs, binding.Error{
FieldNames: []string{"Channel"},
Classification: "",
- Message: ctx.Tr("repo.settings.add_webhook.invalid_channel_name"),
+ Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"),
})
}
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
@@ -598,8 +601,8 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
// swagger:model MergePullRequestOption
type MergePullRequestForm struct {
// required: true
- // enum: merge,rebase,rebase-merge,squash,manually-merged
- Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,manually-merged)"`
+ // enum: merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged
+ Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"`
MergeTitleField string
MergeMessageField string
MergeCommitID string // only used for manually-merged
@@ -625,6 +628,7 @@ type CodeCommentForm struct {
SingleReview bool `form:"single_review"`
Reply int64 `form:"reply"`
LatestCommitID string
+ Files []string
}
// Validate validates the fields
diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go
index 4dd99f9e32..0135684737 100644
--- a/services/forms/repo_tag_form.go
+++ b/services/forms/repo_tag_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
diff --git a/services/forms/runner.go b/services/forms/runner.go
index 6d16cfce49..6abfc66fc2 100644
--- a/services/forms/runner.go
+++ b/services/forms/runner.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index cbab274238..e2e6c208f7 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -10,11 +10,11 @@ import (
"strings"
auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/context"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
@@ -109,11 +109,7 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.
// domains in the whitelist or if it doesn't match any of
// domains in the blocklist, if any such list is not empty.
func (f *RegisterForm) IsEmailDomainAllowed() bool {
- if len(setting.Service.EmailDomainAllowList) == 0 {
- return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, f.Email)
- }
-
- return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, f.Email)
+ return user_model.IsEmailDomainAllowed(f.Email)
}
// MustChangePasswordForm form for updating your password after account creation
@@ -449,3 +445,14 @@ func (f *PackageSettingForm) Validate(req *http.Request, errs binding.Errors) bi
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
+
+type BlockUserForm struct {
+ Action string `binding:"Required;In(block,unblock,note)"`
+ Blockee string `binding:"Required"`
+ Note string
+}
+
+func (f *BlockUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetValidateContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/services/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go
index d8137a8d13..ca1c77e320 100644
--- a/services/forms/user_form_auth_openid.go
+++ b/services/forms/user_form_auth_openid.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware"
+ "code.gitea.io/gitea/services/context"
"gitea.com/go-chi/binding"
)
diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go
index 03e629a553..c21fddf478 100644
--- a/services/forms/user_form_hidden_comments.go
+++ b/services/forms/user_form_hidden_comments.go
@@ -7,8 +7,8 @@ import (
"math/big"
issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/services/context"
)
type hiddenCommentTypeGroupsType map[string][]issues_model.CommentType
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 0f6e2b6c17..b05c210a0c 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
@@ -153,7 +154,7 @@ func (d *DiffLine) GetBlobExcerptQuery() string {
// GetExpandDirection gets DiffLineExpandDirection
func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
- if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
+ if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
return DiffLineExpandNone
}
if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 {
@@ -1181,41 +1182,30 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
for _, diffFile := range diff.Files {
- gotVendor := false
- gotGenerated := false
+ isVendored := optional.None[bool]()
+ isGenerated := optional.None[bool]()
if checker != nil {
attrs, err := checker.CheckPath(diffFile.Name)
if err == nil {
- if vendored, has := attrs["linguist-vendored"]; has {
- if vendored == "set" || vendored == "true" {
- diffFile.IsVendored = true
- gotVendor = true
- } else {
- gotVendor = vendored == "false"
- }
- }
- if generated, has := attrs["linguist-generated"]; has {
- if generated == "set" || generated == "true" {
- diffFile.IsGenerated = true
- gotGenerated = true
- } else {
- gotGenerated = generated == "false"
- }
- }
- if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
- diffFile.Language = language
- } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
- diffFile.Language = language
+ isVendored = git.AttributeToBool(attrs, git.AttributeLinguistVendored)
+ isGenerated = git.AttributeToBool(attrs, git.AttributeLinguistGenerated)
+
+ language := git.TryReadLanguageAttribute(attrs)
+ if language.Has() {
+ diffFile.Language = language.Value()
}
}
}
- if !gotVendor {
- diffFile.IsVendored = analyze.IsVendor(diffFile.Name)
+ if !isVendored.Has() {
+ isVendored = optional.Some(analyze.IsVendor(diffFile.Name))
}
- if !gotGenerated {
- diffFile.IsGenerated = analyze.IsGenerated(diffFile.Name)
+ diffFile.IsVendored = isVendored.Value()
+
+ if !isGenerated.Has() {
+ isGenerated = optional.Some(analyze.IsGenerated(diffFile.Name))
}
+ diffFile.IsGenerated = isGenerated.Value()
tailSection := diffFile.GetTailSection(gitRepo, opts.BeforeCommitID, opts.AfterCommitID)
if tailSection != nil {
diff --git a/services/issue/assignee.go b/services/issue/assignee.go
index 27fc695533..8740a6664a 100644
--- a/services/issue/assignee.go
+++ b/services/issue/assignee.go
@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@@ -113,10 +114,10 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
return err
}
- var pemResult bool
+ canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
+
if isAdd {
- pemResult = permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests)
- if !pemResult {
+ if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) {
return issues_model.ErrNotValidReviewRequest{
Reason: "Reviewer can't read",
UserID: doer.ID,
@@ -124,28 +125,6 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
}
}
- if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
- return nil
- }
-
- pemResult = doer.ID == issue.PosterID
- if !pemResult {
- pemResult = permDoer.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
- }
- if !pemResult {
- pemResult, err = issues_model.IsOfficialReviewer(ctx, issue, doer)
- if err != nil {
- return err
- }
- if !pemResult {
- return issues_model.ErrNotValidReviewRequest{
- Reason: "Doer can't choose reviewer",
- UserID: doer.ID,
- RepoID: issue.Repo.ID,
- }
- }
- }
-
if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 {
return issues_model.ErrNotValidReviewRequest{
Reason: "poster of pr can't be reviewer",
@@ -153,22 +132,35 @@ func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
RepoID: issue.Repo.ID,
}
}
- } else {
- if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
+
+ if canDoerChangeReviewRequests {
return nil
}
- pemResult = permDoer.IsAdmin()
- if !pemResult {
- return issues_model.ErrNotValidReviewRequest{
- Reason: "Doer is not admin",
- UserID: doer.ID,
- RepoID: issue.Repo.ID,
- }
+ if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
+ return nil
+ }
+
+ return issues_model.ErrNotValidReviewRequest{
+ Reason: "Doer can't choose reviewer",
+ UserID: doer.ID,
+ RepoID: issue.Repo.ID,
}
}
- return nil
+ if canDoerChangeReviewRequests {
+ return nil
+ }
+
+ if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
+ return nil
+ }
+
+ return issues_model.ErrNotValidReviewRequest{
+ Reason: "Doer can't remove reviewer",
+ UserID: doer.ID,
+ RepoID: issue.Repo.ID,
+ }
}
// IsValidTeamReviewRequest Check permission for ReviewRequest Team
@@ -181,11 +173,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
}
}
- permission, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
- if err != nil {
- log.Error("Unable to GetUserRepoPermission for %-v in %-v#%d", doer, issue.Repo, issue.Index)
- return err
- }
+ canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue)
if isAdd {
if issue.Repo.IsPrivate {
@@ -200,30 +188,26 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team,
}
}
- doerCanWrite := permission.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
- if !doerCanWrite && doer.ID != issue.PosterID {
- official, err := issues_model.IsOfficialReviewer(ctx, issue, doer)
- if err != nil {
- log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index)
- return err
- }
- if !official {
- return issues_model.ErrNotValidReviewRequest{
- Reason: "Doer can't choose reviewer",
- UserID: doer.ID,
- RepoID: issue.Repo.ID,
- }
- }
+ if canDoerChangeReviewRequests {
+ return nil
}
- } else if !permission.IsAdmin() {
+
return issues_model.ErrNotValidReviewRequest{
- Reason: "Only admin users can remove team requests. Doer is not admin",
+ Reason: "Doer can't choose reviewer",
UserID: doer.ID,
RepoID: issue.Repo.ID,
}
}
- return nil
+ if canDoerChangeReviewRequests {
+ return nil
+ }
+
+ return issues_model.ErrNotValidReviewRequest{
+ Reason: "Doer can't remove reviewer",
+ UserID: doer.ID,
+ RepoID: issue.Repo.ID,
+ }
}
// TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
@@ -242,16 +226,33 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
return nil, nil
}
+ return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment)
+}
+
+func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifers []*ReviewRequestNotifier) {
+ for _, reviewNotifer := range reviewNotifers {
+ if reviewNotifer.Reviwer != nil {
+ notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifer.Reviwer, reviewNotifer.IsAdd, reviewNotifer.Comment)
+ } else if reviewNotifer.ReviewTeam != nil {
+ if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifer.ReviewTeam, reviewNotifer.IsAdd, reviewNotifer.Comment); err != nil {
+ log.Error("teamReviewRequestNotify: %v", err)
+ }
+ }
+ }
+}
+
+// teamReviewRequestNotify notify all user in this team
+func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error {
// notify all user in this team
if err := comment.LoadIssue(ctx); err != nil {
- return nil, err
+ return err
}
members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
TeamID: reviewer.ID,
})
if err != nil {
- return nil, err
+ return err
}
for _, member := range members {
@@ -262,5 +263,52 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment)
}
- return comment, err
+ return err
+}
+
+// CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
+func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool {
+ // The poster of the PR can change the reviewers
+ if doer.ID == issue.PosterID {
+ return true
+ }
+
+ // The owner of the repo can change the reviewers
+ if doer.ID == repo.OwnerID {
+ return true
+ }
+
+ // Collaborators of the repo can change the reviewers
+ isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID)
+ if err != nil {
+ log.Error("IsCollaborator: %v", err)
+ return false
+ }
+ if isCollaborator {
+ return true
+ }
+
+ // If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
+ if repo.Owner.IsOrganization() {
+ teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
+ if err != nil {
+ log.Error("GetTeamsWithAccessToRepo: %v", err)
+ return false
+ }
+ for _, team := range teams {
+ if !team.UnitEnabled(ctx, unit.TypePullRequests) {
+ continue
+ }
+ isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID)
+ if err != nil {
+ log.Error("IsTeamMember: %v", err)
+ continue
+ }
+ if isMember {
+ return true
+ }
+ }
+ }
+
+ return false
}
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 8d8c575c14..d68623aff6 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -9,6 +9,7 @@ import (
"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"
"code.gitea.io/gitea/modules/timeutil"
@@ -21,6 +22,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
return fmt.Errorf("cannot create reference with empty commit SHA")
}
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
// Check if same reference from same commit has already existed.
has, err := db.GetEngine(ctx).Get(&issues_model.Comment{
Type: issues_model.CommentTypeCommitRef,
@@ -46,6 +53,12 @@ func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_mod
// CreateIssueComment creates a plain issue comment.
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) {
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, repo, doer); !isAdmin {
+ return nil, user_model.ErrBlockedUser
+ }
+ }
+
comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeComment,
Doer: doer,
@@ -70,6 +83,19 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
// UpdateComment updates information of comment.
func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
+ if err := c.LoadIssue(ctx); err != nil {
+ return err
+ }
+ if err := c.Issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, c.Issue.PosterID, c.Issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, c.Issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
if needsContentHistory {
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
diff --git a/services/issue/commit.go b/services/issue/commit.go
index e493a03211..0a59088d12 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -5,6 +5,7 @@ package issue
import (
"context"
+ "errors"
"fmt"
"html"
"net/url"
@@ -160,6 +161,9 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
message := fmt.Sprintf(`%s`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ continue
+ }
return err
}
diff --git a/services/issue/content.go b/services/issue/content.go
index 6e56714ddf..2f9bee806a 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -7,12 +7,23 @@ import (
"context"
issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
notify_service "code.gitea.io/gitea/services/notify"
)
// ChangeContent changes issue content, as the given user.
-func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
+func ChangeContent(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string) error {
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
oldContent := issue.Content
if err := issues_model.ChangeIssueContent(ctx, issue, doer, content); err != nil {
diff --git a/services/issue/issue.go b/services/issue/issue.go
index b1f418c32e..c7fa9f3300 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -15,21 +15,40 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
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/log"
"code.gitea.io/gitea/modules/storage"
notify_service "code.gitea.io/gitea/services/notify"
)
// NewIssue creates new issue with labels for repository.
-func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
- if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
+func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error {
+ if err := issue.LoadPoster(ctx); err != nil {
return err
}
- for _, assigneeID := range assigneeIDs {
- if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
+ if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+ return user_model.ErrBlockedUser
+ }
+
+ if err := db.WithTx(ctx, func(ctx context.Context) error {
+ if err := issues_model.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil {
return err
}
+ for _, assigneeID := range assigneeIDs {
+ if _, err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID, true); err != nil {
+ return err
+ }
+ }
+ if projectID > 0 {
+ if err := issues_model.ChangeProjectAssign(ctx, issue, issue.Poster, projectID); err != nil {
+ return err
+ }
+ }
+ return nil
+ }); err != nil {
+ return err
}
mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
@@ -57,17 +76,31 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return nil
}
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ if isAdmin, _ := access_model.IsUserRepoAdmin(ctx, issue.Repo, doer); !isAdmin {
+ return user_model.ErrBlockedUser
+ }
+ }
+
if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
return err
}
+ var reviewNotifers []*ReviewRequestNotifier
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
- if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest); err != nil {
- return err
+ var err error
+ reviewNotifers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
+ if err != nil {
+ log.Error("PullRequestCodeOwnersReview: %v", err)
}
}
notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle)
+ ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers)
return nil
}
@@ -93,31 +126,25 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
- var allNewAssignees []*user_model.User
+ uniqueAssignees := container.SetOf(multipleAssignees...)
// Keep the old assignee thingy for compatibility reasons
if oneAssignee != "" {
- // Prevent double adding assignees
- var isDouble bool
- for _, assignee := range multipleAssignees {
- if assignee == oneAssignee {
- isDouble = true
- break
- }
- }
-
- if !isDouble {
- multipleAssignees = append(multipleAssignees, oneAssignee)
- }
+ uniqueAssignees.Add(oneAssignee)
}
// Loop through all assignees to add them
- for _, assigneeName := range multipleAssignees {
+ allNewAssignees := make([]*user_model.User, 0, len(uniqueAssignees))
+ for _, assigneeName := range uniqueAssignees.Values() {
assignee, err := user_model.GetUserByName(ctx, assigneeName)
if err != nil {
return err
}
+ if user_model.IsUserBlockedBy(ctx, doer, assignee.ID) {
+ return user_model.ErrBlockedUser
+ }
+
allNewAssignees = append(allNewAssignees, assignee)
}
diff --git a/services/issue/pull.go b/services/issue/pull.go
new file mode 100644
index 0000000000..b7b63a7024
--- /dev/null
+++ b/services/issue/pull.go
@@ -0,0 +1,147 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ org_model "code.gitea.io/gitea/models/organization"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+func getMergeBase(repo *git.Repository, pr *issues_model.PullRequest, baseBranch, headBranch string) (string, error) {
+ // Add a temporary remote
+ tmpRemote := fmt.Sprintf("mergebase-%d-%d", pr.ID, time.Now().UnixNano())
+ if err := repo.AddRemote(tmpRemote, repo.Path, false); err != nil {
+ return "", fmt.Errorf("AddRemote: %w", err)
+ }
+ defer func() {
+ if err := repo.RemoveRemote(tmpRemote); err != nil {
+ log.Error("getMergeBase: RemoveRemote: %v", err)
+ }
+ }()
+
+ mergeBase, _, err := repo.GetMergeBase(tmpRemote, baseBranch, headBranch)
+ return mergeBase, err
+}
+
+type ReviewRequestNotifier struct {
+ Comment *issues_model.Comment
+ IsAdd bool
+ Reviwer *user_model.User
+ ReviewTeam *org_model.Team
+}
+
+func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
+ files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
+
+ if pr.IsWorkInProgress(ctx) {
+ return nil, nil
+ }
+
+ if err := pr.LoadHeadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ if pr.HeadRepo.IsFork {
+ return nil, nil
+ }
+
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+ if err != nil {
+ return nil, err
+ }
+ defer repo.Close()
+
+ commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
+ if err != nil {
+ return nil, err
+ }
+
+ var data string
+ for _, file := range files {
+ if blob, err := commit.GetBlobByPath(file); err == nil {
+ data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
+ if err == nil {
+ break
+ }
+ }
+ }
+
+ rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
+
+ // get the mergebase
+ mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
+ if err != nil {
+ return nil, err
+ }
+
+ // https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
+ // between the merge base and the head commit but not the base branch and the head commit
+ changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitRefName())
+ if err != nil {
+ return nil, err
+ }
+
+ uniqUsers := make(map[int64]*user_model.User)
+ uniqTeams := make(map[string]*org_model.Team)
+ for _, rule := range rules {
+ for _, f := range changedFiles {
+ if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) {
+ for _, u := range rule.Users {
+ uniqUsers[u.ID] = u
+ }
+ for _, t := range rule.Teams {
+ uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t
+ }
+ }
+ }
+ }
+
+ notifiers := make([]*ReviewRequestNotifier, 0, len(uniqUsers)+len(uniqTeams))
+
+ if err := issue.LoadPoster(ctx); err != nil {
+ return nil, err
+ }
+
+ for _, u := range uniqUsers {
+ if u.ID != issue.Poster.ID {
+ comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
+ if err != nil {
+ log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
+ return nil, err
+ }
+ notifiers = append(notifiers, &ReviewRequestNotifier{
+ Comment: comment,
+ IsAdd: true,
+ Reviwer: u,
+ })
+ }
+ }
+ for _, t := range uniqTeams {
+ comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
+ if err != nil {
+ log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
+ return nil, err
+ }
+ notifiers = append(notifiers, &ReviewRequestNotifier{
+ Comment: comment,
+ IsAdd: true,
+ ReviewTeam: t,
+ })
+ }
+
+ return notifiers, nil
+}
diff --git a/services/issue/reaction.go b/services/issue/reaction.go
new file mode 100644
index 0000000000..deb99169e1
--- /dev/null
+++ b/services/issue/reaction.go
@@ -0,0 +1,50 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+// CreateIssueReaction creates a reaction on an issue.
+func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, content string) (*issues_model.Reaction, error) {
+ if err := issue.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, issue.PosterID, issue.Repo.OwnerID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
+ return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+ Type: content,
+ DoerID: doer.ID,
+ IssueID: issue.ID,
+ })
+}
+
+// CreateCommentReaction creates a reaction on a comment.
+func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, content string) (*issues_model.Reaction, error) {
+ if err := comment.LoadIssue(ctx); err != nil {
+ return nil, err
+ }
+
+ if err := comment.Issue.LoadRepo(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, comment.Issue.PosterID, comment.Issue.Repo.OwnerID, comment.PosterID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
+ return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
+ Type: content,
+ DoerID: doer.ID,
+ IssueID: comment.Issue.ID,
+ CommentID: comment.ID,
+ })
+}
diff --git a/models/issues/reaction_test.go b/services/issue/reaction_test.go
similarity index 65%
rename from models/issues/reaction_test.go
rename to services/issue/reaction_test.go
index 5dc8e1a5f3..7734860fc0 100644
--- a/models/issues/reaction_test.go
+++ b/services/issue/reaction_test.go
@@ -1,7 +1,7 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
-package issues_test
+package issue
import (
"testing"
@@ -16,13 +16,13 @@ import (
"github.com/stretchr/testify/assert"
)
-func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
+func addReaction(t *testing.T, doer *user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, content string) {
var reaction *issues_model.Reaction
var err error
- if commentID == 0 {
- reaction, err = issues_model.CreateIssueReaction(db.DefaultContext, doerID, issueID, content)
+ if comment == nil {
+ reaction, err = CreateIssueReaction(db.DefaultContext, doer, issue, content)
} else {
- reaction, err = issues_model.CreateCommentReaction(db.DefaultContext, doerID, issueID, commentID, content)
+ reaction, err = CreateCommentReaction(db.DefaultContext, doer, comment, content)
}
assert.NoError(t, err)
assert.NotNil(t, reaction)
@@ -32,32 +32,26 @@ func TestIssueAddReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- var issue1ID int64 = 1
+ addReaction(t, user1, issue, nil, "heart")
- addReaction(t, user1.ID, issue1ID, 0, "heart")
-
- unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
}
func TestIssueAddDuplicateReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- var issue1ID int64 = 1
+ addReaction(t, user1, issue, nil, "heart")
- addReaction(t, user1.ID, issue1ID, 0, "heart")
-
- reaction, err := issues_model.CreateReaction(db.DefaultContext, &issues_model.ReactionOptions{
- DoerID: user1.ID,
- IssueID: issue1ID,
- Type: "heart",
- })
+ reaction, err := CreateIssueReaction(db.DefaultContext, user1, issue, "heart")
assert.Error(t, err)
assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
- existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+ existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
assert.Equal(t, existingR.ID, reaction.ID)
}
@@ -65,15 +59,14 @@ func TestIssueDeleteReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
- var issue1ID int64 = 1
+ addReaction(t, user1, issue, nil, "heart")
- addReaction(t, user1.ID, issue1ID, 0, "heart")
-
- err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue1ID, "heart")
+ err := issues_model.DeleteIssueReaction(db.DefaultContext, user1.ID, issue.ID, "heart")
assert.NoError(t, err)
- unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
+ unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue.ID})
}
func TestIssueReactionCount(t *testing.T) {
@@ -87,19 +80,19 @@ func TestIssueReactionCount(t *testing.T) {
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
ghost := user_model.NewGhostUser()
- var issueID int64 = 2
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
- addReaction(t, user1.ID, issueID, 0, "heart")
- addReaction(t, user2.ID, issueID, 0, "heart")
- addReaction(t, org3.ID, issueID, 0, "heart")
- addReaction(t, org3.ID, issueID, 0, "+1")
- addReaction(t, user4.ID, issueID, 0, "+1")
- addReaction(t, user4.ID, issueID, 0, "heart")
- addReaction(t, ghost.ID, issueID, 0, "-1")
+ addReaction(t, user1, issue, nil, "heart")
+ addReaction(t, user2, issue, nil, "heart")
+ addReaction(t, org3, issue, nil, "heart")
+ addReaction(t, org3, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "heart")
+ addReaction(t, ghost, issue, nil, "-1")
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
- IssueID: issueID,
+ IssueID: issue.ID,
})
assert.NoError(t, err)
assert.Len(t, reactionsList, 7)
@@ -122,13 +115,11 @@ func TestIssueCommentAddReaction(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
- var issue1ID int64 = 1
- var comment1ID int64 = 1
+ addReaction(t, user1, nil, comment, "heart")
- addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
-
- unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
}
func TestIssueCommentDeleteReaction(t *testing.T) {
@@ -139,17 +130,16 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
- var issue1ID int64 = 1
- var comment1ID int64 = 1
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
- addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
- addReaction(t, user2.ID, issue1ID, comment1ID, "heart")
- addReaction(t, org3.ID, issue1ID, comment1ID, "heart")
- addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
+ addReaction(t, user1, nil, comment, "heart")
+ addReaction(t, user2, nil, comment, "heart")
+ addReaction(t, org3, nil, comment, "heart")
+ addReaction(t, user4, nil, comment, "+1")
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
- IssueID: issue1ID,
- CommentID: comment1ID,
+ IssueID: comment.IssueID,
+ CommentID: comment.ID,
})
assert.NoError(t, err)
assert.Len(t, reactionsList, 4)
@@ -163,12 +153,10 @@ func TestIssueCommentReactionCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1})
- var issue1ID int64 = 1
- var comment1ID int64 = 1
+ addReaction(t, user1, nil, comment, "heart")
+ assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, comment.IssueID, comment.ID, "heart"))
- addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
- assert.NoError(t, issues_model.DeleteCommentReaction(db.DefaultContext, user1.ID, issue1ID, comment1ID, "heart"))
-
- unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
+ unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: comment.IssueID, CommentID: comment.ID})
}
diff --git a/services/issue/template.go b/services/issue/template.go
index b6ae077987..dd9d015f0f 100644
--- a/services/issue/template.go
+++ b/services/issue/template.go
@@ -109,21 +109,23 @@ func IsTemplateConfig(path string) bool {
return false
}
-// GetTemplatesFromDefaultBranch checks for issue templates in the repo's default branch,
-// returns valid templates and the errors of invalid template files.
-func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) ([]*api.IssueTemplate, map[string]error) {
- var issueTemplates []*api.IssueTemplate
-
+// ParseTemplatesFromDefaultBranch parses the issue templates in the repo's default branch,
+// returns valid templates and the errors of invalid template files (the errors map is guaranteed to be non-nil).
+func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (ret struct {
+ IssueTemplates []*api.IssueTemplate
+ TemplateErrors map[string]error
+},
+) {
+ ret.TemplateErrors = map[string]error{}
if repo.IsEmpty {
- return issueTemplates, nil
+ return ret
}
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
- return issueTemplates, nil
+ return ret
}
- invalidFiles := map[string]error{}
for _, dirName := range templateDirCandidates {
tree, err := commit.SubTree(dirName)
if err != nil {
@@ -133,7 +135,7 @@ func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repositor
entries, err := tree.ListEntries()
if err != nil {
log.Debug("list entries in %s: %v", dirName, err)
- return issueTemplates, nil
+ return ret
}
for _, entry := range entries {
if !template.CouldBe(entry.Name()) {
@@ -141,16 +143,16 @@ func GetTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repositor
}
fullName := path.Join(dirName, entry.Name())
if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
- invalidFiles[fullName] = err
+ ret.TemplateErrors[fullName] = err
} else {
if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/
it.Ref = git.BranchPrefix + it.Ref
}
- issueTemplates = append(issueTemplates, it)
+ ret.IssueTemplates = append(ret.IssueTemplates, it)
}
}
}
- return issueTemplates, invalidFiles
+ return ret
}
// GetTemplateConfigFromDefaultBranch returns the issue config for this repo.
@@ -179,8 +181,8 @@ func GetTemplateConfigFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repo
}
func HasTemplatesOrContactLinks(repo *repo.Repository, gitRepo *git.Repository) bool {
- ret, _ := GetTemplatesFromDefaultBranch(repo, gitRepo)
- if len(ret) > 0 {
+ ret := ParseTemplatesFromDefaultBranch(repo, gitRepo)
+ if len(ret.IssueTemplates) > 0 {
return true
}
diff --git a/services/lfs/locks.go b/services/lfs/locks.go
index 08d7432656..2a362b1c0d 100644
--- a/services/lfs/locks.go
+++ b/services/lfs/locks.go
@@ -11,12 +11,12 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
lfs_module "code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 62134b7d60..706be0d080 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -5,6 +5,7 @@ package lfs
import (
stdCtx "context"
+ "crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
@@ -25,15 +26,14 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
lfs_module "code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/services/context"
"github.com/golang-jwt/jwt/v5"
- "github.com/minio/sha256-simd"
)
// requestContext contain variables from the HTTP request.
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
index 9682c52456..dc0b539822 100644
--- a/services/mailer/incoming/incoming_handler.go
+++ b/services/mailer/incoming/incoming_handler.go
@@ -14,9 +14,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
attachment_service "code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/context/upload"
issue_service "code.gitea.io/gitea/services/issue"
incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
"code.gitea.io/gitea/services/mailer/token"
@@ -130,6 +130,7 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u
false, // not pending review but a single review
comment.ReviewID,
"",
+ nil,
)
if err != nil {
return fmt.Errorf("CreateCodeComment failed: %w", err)
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index ca27336f92..a63ba7a52a 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -94,7 +94,7 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
// No mail service configured
return
}
- sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.activate_account"), "activate account")
+ sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
}
// SendResetPasswordMail sends a password reset mail to the user
@@ -104,7 +104,7 @@ func SendResetPasswordMail(u *user_model.User) {
return
}
locale := translation.NewLocale(u.Language)
- sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.reset_password"), "recover account")
+ sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
}
// SendActivateEmailMail sends confirmation email to confirm new email address
@@ -130,7 +130,7 @@ func SendActivateEmailMail(u *user_model.User, email string) {
return
}
- msg := NewMessage(email, locale.Tr("mail.activate_email"), content.String())
+ msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String())
msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
SendAsync(msg)
@@ -158,7 +158,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
return
}
- msg := NewMessage(u.Email, locale.Tr("mail.register_notify"), content.String())
+ msg := NewMessage(u.Email, locale.TrString("mail.register_notify"), content.String())
msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
SendAsync(msg)
@@ -173,7 +173,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
locale := translation.NewLocale(u.Language)
repoName := repo.FullName()
- subject := locale.Tr("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
+ subject := locale.TrString("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
data := map[string]any{
"locale": locale,
"Subject": subject,
@@ -222,7 +222,8 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
body, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
- Base: ctx.Issue.Repo.HTMLURL(),
+ AbsolutePrefix: true,
+ Base: ctx.Issue.Repo.HTMLURL(),
},
Metas: ctx.Issue.Repo.ComposeMetas(ctx),
}, ctx.Content)
diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go
index 5e8e5b6af3..6682774a04 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -68,7 +68,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
return
}
- subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
+ subject := locale.TrString("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
mailMeta := map[string]any{
"locale": locale,
"Release": rel,
diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go
index b89dcd43b5..e0d55bb120 100644
--- a/services/mailer/mail_repo.go
+++ b/services/mailer/mail_repo.go
@@ -56,11 +56,11 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
content bytes.Buffer
)
- destination := locale.Tr("mail.repo.transfer.to_you")
- subject := locale.Tr("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName())
+ destination := locale.TrString("mail.repo.transfer.to_you")
+ subject := locale.TrString("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName())
if newOwner.IsOrganization() {
destination = newOwner.DisplayName()
- subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
+ subject = locale.TrString("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
}
data := map[string]any{
diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go
index ab32beefac..ceecefa50f 100644
--- a/services/mailer/mail_team_invite.go
+++ b/services/mailer/mail_team_invite.go
@@ -50,7 +50,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect)
}
- subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
+ subject := locale.TrString("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
mailMeta := map[string]any{
"locale": locale,
"Inviter": inviter,
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index e300aeccb0..d87c57ffe7 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -8,6 +8,8 @@ import (
"context"
"fmt"
"html/template"
+ "io"
+ "mime/quotedprintable"
"regexp"
"strings"
"testing"
@@ -19,6 +21,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -67,6 +70,12 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
func TestComposeIssueCommentMessage(t *testing.T) {
doer, _, issue, comment := prepareMailerTest(t)
+ markup.Init(&markup.ProcessorHelper{
+ IsUsernameMentionable: func(ctx context.Context, username string) bool {
+ return username == doer.Name
+ },
+ })
+
setting.IncomingEmail.Enabled = true
defer func() { setting.IncomingEmail.Enabled = false }()
@@ -77,7 +86,8 @@ func TestComposeIssueCommentMessage(t *testing.T) {
msgs, err := composeIssueCommentMessages(&mailCommentContext{
Context: context.TODO(), // TODO: use a correct context
Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
- Content: "test body", Comment: comment,
+ Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
+ Comment: comment,
}, "en-US", recipients, false, "issue comment")
assert.NoError(t, err)
assert.Len(t, msgs, 2)
@@ -96,6 +106,20 @@ func TestComposeIssueCommentMessage(t *testing.T) {
assert.Equal(t, "", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match")
assert.Equal(t, "", gomailMsg.GetHeader("List-Post")[0])
assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto
+
+ var buf bytes.Buffer
+ gomailMsg.WriteTo(&buf)
+
+ b, err := io.ReadAll(quotedprintable.NewReader(&buf))
+ assert.NoError(t, err)
+
+ // text/plain
+ assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL()))
+ assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL()))
+
+ // text/html
+ assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL()))
+ assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL()))
}
func TestComposeIssueMessage(t *testing.T) {
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go
index aa7b567188..8a5a762d6b 100644
--- a/services/mailer/token/token.go
+++ b/services/mailer/token/token.go
@@ -6,14 +6,13 @@ package token
import (
"context"
crypto_hmac "crypto/hmac"
+ "crypto/sha256"
"encoding/base32"
"fmt"
"time"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
-
- "github.com/minio/sha256-simd"
)
// A token is a verifiable container describing an action.
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
index 89fe3e7e34..5553ebc058 100644
--- a/services/markup/main_test.go
+++ b/services/markup/main_test.go
@@ -11,6 +11,6 @@ import (
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
- FixtureFiles: []string{"user.yml"},
+ FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
})
}
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 3551f85c46..68487fb8db 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -7,13 +7,15 @@ import (
"context"
"code.gitea.io/gitea/models/user"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
+ gitea_context "code.gitea.io/gitea/services/context"
)
func ProcessorHelper() *markup.ProcessorHelper {
return &markup.ProcessorHelper{
ElementDir: "auto", // set dir="auto" for necessary (eg: , , etc) tags
+
+ RenderRepoFileCodePreview: renderRepoFileCodePreview,
IsUsernameMentionable: func(ctx context.Context, username string) bool {
mentionedUser, err := user.GetUserByName(ctx, username)
if err != nil {
diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/processorhelper_codepreview.go
new file mode 100644
index 0000000000..ef95046128
--- /dev/null
+++ b/services/markup/processorhelper_codepreview.go
@@ -0,0 +1,117 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "html/template"
+ "strings"
+
+ "code.gitea.io/gitea/models/perm/access"
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/indexer/code"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
+ gitea_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+ opts.LineStop = max(opts.LineStop, opts.LineStart)
+ lineCount := opts.LineStop - opts.LineStart + 1
+ if lineCount <= 0 || lineCount > 140 /* GitHub at most show 140 lines */ {
+ lineCount = 10
+ opts.LineStop = opts.LineStart + lineCount
+ }
+
+ dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
+ if err != nil {
+ return "", err
+ }
+
+ webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
+ if !ok {
+ return "", fmt.Errorf("context is not a web context")
+ }
+ doer := webCtx.Doer
+
+ perms, err := access.GetUserRepoPermission(ctx, dbRepo, doer)
+ if err != nil {
+ return "", err
+ }
+ if !perms.CanRead(unit.TypeCode) {
+ return "", fmt.Errorf("no permission")
+ }
+
+ gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
+ if err != nil {
+ return "", err
+ }
+ defer gitRepo.Close()
+
+ commit, err := gitRepo.GetCommit(opts.CommitID)
+ if err != nil {
+ return "", err
+ }
+
+ language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath)
+ blob, err := commit.GetBlobByPath(opts.FilePath)
+ if err != nil {
+ return "", err
+ }
+
+ if blob.Size() > setting.UI.MaxDisplayFileSize {
+ return "", fmt.Errorf("file is too large")
+ }
+
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ return "", err
+ }
+ defer dataRc.Close()
+
+ reader := bufio.NewReader(dataRc)
+ for i := 1; i < opts.LineStart; i++ {
+ if _, err = reader.ReadBytes('\n'); err != nil {
+ return "", err
+ }
+ }
+
+ lineNums := make([]int, 0, lineCount)
+ lineCodes := make([]string, 0, lineCount)
+ for i := opts.LineStart; i <= opts.LineStop; i++ {
+ if line, err := reader.ReadString('\n'); err != nil && line == "" {
+ break
+ } else {
+ lineNums = append(lineNums, i)
+ lineCodes = append(lineCodes, line)
+ }
+ }
+ realLineStop := max(opts.LineStart, opts.LineStart+len(lineNums)-1)
+ highlightLines := code.HighlightSearchResultCode(opts.FilePath, language, lineNums, strings.Join(lineCodes, ""))
+
+ escapeStatus := &charset.EscapeStatus{}
+ lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
+ for i, hl := range highlightLines {
+ lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, webCtx.Base.Locale, charset.RuneNBSP)
+ escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
+ }
+
+ return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{
+ "FullURL": opts.FullURL,
+ "FilePath": opts.FilePath,
+ "LineStart": opts.LineStart,
+ "LineStop": realLineStop,
+ "RepoLink": dbRepo.Link(),
+ "CommitID": opts.CommitID,
+ "HighlightLines": highlightLines,
+ "EscapeStatus": escapeStatus,
+ "LineEscapeStatus": lineEscapeStatus,
+ })
+}
diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/processorhelper_codepreview_test.go
new file mode 100644
index 0000000000..154e4e8e44
--- /dev/null
+++ b/services/markup/processorhelper_codepreview_test.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestProcessorHelperCodePreview(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ htm, err := renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user2",
+ RepoName: "repo1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ LineStop: 2,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, `
+
+ /README.md
+ repo.code_preview_line_from_to:1,2,65f1bf27bc
+
+
+
+
+ # repo1
+
+
+
+
+
+
+`, string(htm))
+
+ ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ htm, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user2",
+ RepoName: "repo1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, `
+
+ /README.md
+ repo.code_preview_line_in:1,65f1bf27bc
+
+
+
+
+ # repo1
+
+
+
+`, string(htm))
+
+ ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ _, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user15",
+ RepoName: "big_test_private_1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ LineStop: 10,
+ })
+ assert.ErrorContains(t, err, "no permission")
+}
diff --git a/services/markup/processorhelper_test.go b/services/markup/processorhelper_test.go
index ef8f562245..170edae0e0 100644
--- a/services/markup/processorhelper_test.go
+++ b/services/markup/processorhelper_test.go
@@ -12,8 +12,8 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/user"
- gitea_context "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/contexttest"
+ gitea_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go
index 5f11555839..4fe9e30a39 100644
--- a/services/migrations/gitbucket.go
+++ b/services/migrations/gitbucket.go
@@ -72,6 +72,11 @@ func (g *GitBucketDownloader) LogString() string {
// NewGitBucketDownloader creates a GitBucket downloader
func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader {
githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName)
+ // Gitbucket 4.40 uses different internal hard-coded perPage values.
+ // Issues, PRs, and other major parts use 25. Release page uses 10.
+ // Some API doesn't support paging yet. Sounds difficult, but using
+ // minimum number among them worked out very well.
+ githubDownloader.maxPerPage = 10
githubDownloader.SkipReactions = true
githubDownloader.SkipReviews = true
return &GitBucketDownloader{
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 7b21d9f4d2..87691bf729 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -120,7 +120,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
r.DefaultBranch = repo.DefaultBranch
r.Description = repo.Description
- r, err = repo_module.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
+ r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
RepoName: g.repoName,
Description: repo.Description,
OriginalURL: repo.OriginalURL,
@@ -140,8 +140,18 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
if err != nil {
return err
}
- g.gitRepo, err = gitrepo.OpenRepository(g.ctx, r)
- return err
+ g.gitRepo, err = gitrepo.OpenRepository(g.ctx, g.repo)
+ if err != nil {
+ return err
+ }
+
+ // detect object format from git repository and update to database
+ objectFormat, err := g.gitRepo.GetObjectFormat()
+ if err != nil {
+ return err
+ }
+ g.repo.ObjectFormatName = objectFormat.Name()
+ return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "object_format_name")
}
// Close closes this uploader
@@ -473,6 +483,10 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
}
switch cm.Type {
+ case issues_model.CommentTypeReopen:
+ cm.Content = ""
+ case issues_model.CommentTypeClose:
+ cm.Content = ""
case issues_model.CommentTypeAssignees:
if assigneeID, ok := comment.Meta["AssigneeID"].(int); ok {
cm.AssigneeID = int64(assigneeID)
@@ -482,11 +496,21 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
}
case issues_model.CommentTypeChangeTitle:
if comment.Meta["OldTitle"] != nil {
- cm.OldTitle = fmt.Sprintf("%s", comment.Meta["OldTitle"])
+ cm.OldTitle = fmt.Sprint(comment.Meta["OldTitle"])
}
if comment.Meta["NewTitle"] != nil {
- cm.NewTitle = fmt.Sprintf("%s", comment.Meta["NewTitle"])
+ cm.NewTitle = fmt.Sprint(comment.Meta["NewTitle"])
}
+ case issues_model.CommentTypeChangeTargetBranch:
+ if comment.Meta["OldRef"] != nil && comment.Meta["NewRef"] != nil {
+ cm.OldRef = fmt.Sprint(comment.Meta["OldRef"])
+ cm.NewRef = fmt.Sprint(comment.Meta["NewRef"])
+ cm.Content = ""
+ }
+ case issues_model.CommentTypeMergePull:
+ cm.Content = ""
+ case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge:
+ cm.Content = ""
default:
}
@@ -894,7 +918,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
comment.UpdatedAt = comment.CreatedAt
}
- objectFormat, _ := g.gitRepo.GetObjectFormat()
+ objectFormat := git.ObjectFormatFromName(g.repo.ObjectFormatName)
if !objectFormat.IsValid(comment.CommitID) {
log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID)
comment.CommitID = headCommitID
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index c8102c6b8b..c9b9248098 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -23,9 +23,9 @@ import (
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -68,14 +68,14 @@ func TestGiteaUploadRepo(t *testing.T) {
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
- IsClosed: util.OptionalBoolFalse,
+ IsClosed: optional.Some(false),
})
assert.NoError(t, err)
assert.Len(t, milestones, 1)
milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
- IsClosed: util.OptionalBoolTrue,
+ IsClosed: optional.Some(true),
})
assert.NoError(t, err)
assert.Empty(t, milestones)
@@ -108,7 +108,7 @@ func TestGiteaUploadRepo(t *testing.T) {
issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
RepoIDs: []int64{repo.ID},
- IsPull: util.OptionalBoolFalse,
+ IsPull: optional.Some(false),
SortType: "oldest",
})
assert.NoError(t, err)
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 3db10465fc..bbc44e958a 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -11,9 +11,11 @@ import (
"net/http"
"net/url"
"path"
+ "regexp"
"strings"
"time"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
@@ -506,30 +508,8 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err)
}
for _, comment := range comments {
- // Flatten comment threads
- if !comment.IndividualNote {
- for _, note := range comment.Notes {
- allComments = append(allComments, &base.Comment{
- IssueIndex: commentable.GetLocalIndex(),
- Index: int64(note.ID),
- PosterID: int64(note.Author.ID),
- PosterName: note.Author.Username,
- PosterEmail: note.Author.Email,
- Content: note.Body,
- Created: *note.CreatedAt,
- })
- }
- } else {
- c := comment.Notes[0]
- allComments = append(allComments, &base.Comment{
- IssueIndex: commentable.GetLocalIndex(),
- Index: int64(c.ID),
- PosterID: int64(c.Author.ID),
- PosterName: c.Author.Username,
- PosterEmail: c.Author.Email,
- Content: c.Body,
- Created: *c.CreatedAt,
- })
+ for _, note := range comment.Notes {
+ allComments = append(allComments, g.convertNoteToComment(commentable.GetLocalIndex(), note))
}
}
if resp.NextPage == 0 {
@@ -537,9 +517,93 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
}
page = resp.NextPage
}
+
+ page = 1
+ for {
+ var stateEvents []*gitlab.StateEvent
+ var resp *gitlab.Response
+ var err error
+ if context.IsMergeRequest {
+ stateEvents, resp, err = g.client.ResourceStateEvents.ListMergeStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
+ ListOptions: gitlab.ListOptions{
+ Page: page,
+ PerPage: g.maxPerPage,
+ },
+ }, nil, gitlab.WithContext(g.ctx))
+ } else {
+ stateEvents, resp, err = g.client.ResourceStateEvents.ListIssueStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
+ ListOptions: gitlab.ListOptions{
+ Page: page,
+ PerPage: g.maxPerPage,
+ },
+ }, nil, gitlab.WithContext(g.ctx))
+ }
+ if err != nil {
+ return nil, false, fmt.Errorf("error while listing state events: %v %w", g.repoID, err)
+ }
+
+ for _, stateEvent := range stateEvents {
+ comment := &base.Comment{
+ IssueIndex: commentable.GetLocalIndex(),
+ Index: int64(stateEvent.ID),
+ PosterID: int64(stateEvent.User.ID),
+ PosterName: stateEvent.User.Username,
+ Content: "",
+ Created: *stateEvent.CreatedAt,
+ }
+ switch stateEvent.State {
+ case gitlab.ClosedEventType:
+ comment.CommentType = issues_model.CommentTypeClose.String()
+ case gitlab.MergedEventType:
+ comment.CommentType = issues_model.CommentTypeMergePull.String()
+ case gitlab.ReopenedEventType:
+ comment.CommentType = issues_model.CommentTypeReopen.String()
+ default:
+ // Ignore other event types
+ continue
+ }
+ allComments = append(allComments, comment)
+ }
+
+ if resp.NextPage == 0 {
+ break
+ }
+ page = resp.NextPage
+ }
+
return allComments, true, nil
}
+var targetBranchChangeRegexp = regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$")
+
+func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment {
+ comment := &base.Comment{
+ IssueIndex: localIndex,
+ Index: int64(note.ID),
+ PosterID: int64(note.Author.ID),
+ PosterName: note.Author.Username,
+ PosterEmail: note.Author.Email,
+ Content: note.Body,
+ Created: *note.CreatedAt,
+ Meta: map[string]any{},
+ }
+
+ // Try to find the underlying event of system notes.
+ if note.System {
+ if match := targetBranchChangeRegexp.FindStringSubmatch(note.Body); match != nil {
+ comment.CommentType = issues_model.CommentTypeChangeTargetBranch.String()
+ comment.Meta["OldRef"] = match[1]
+ comment.Meta["NewRef"] = match[2]
+ } else if strings.HasPrefix(note.Body, "enabled an automatic merge") {
+ comment.CommentType = issues_model.CommentTypePRScheduledToAutoMerge.String()
+ } else if note.Body == "canceled the automatic merge" {
+ comment.CommentType = issues_model.CommentTypePRUnScheduledToAutoMerge.String()
+ }
+ }
+
+ return comment
+}
+
// GetPullRequests returns pull requests according page and perPage
func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
if perPage > g.maxPerPage {
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 1e0aa2b025..0b9eeaed54 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -517,6 +517,88 @@ func TestAwardsToReactions(t *testing.T) {
}, reactions)
}
+func TestNoteToComment(t *testing.T) {
+ downloader := &GitlabDownloader{}
+
+ now := time.Now()
+ makeTestNote := func(id int, body string, system bool) gitlab.Note {
+ return gitlab.Note{
+ ID: id,
+ Author: struct {
+ ID int `json:"id"`
+ Username string `json:"username"`
+ Email string `json:"email"`
+ Name string `json:"name"`
+ State string `json:"state"`
+ AvatarURL string `json:"avatar_url"`
+ WebURL string `json:"web_url"`
+ }{
+ ID: 72,
+ Email: "test@example.com",
+ Username: "test",
+ },
+ Body: body,
+ CreatedAt: &now,
+ System: system,
+ }
+ }
+ notes := []gitlab.Note{
+ makeTestNote(1, "This is a regular comment", false),
+ makeTestNote(2, "enabled an automatic merge for abcd1234", true),
+ makeTestNote(3, "changed target branch from `master` to `main`", true),
+ makeTestNote(4, "canceled the automatic merge", true),
+ }
+ comments := []base.Comment{{
+ IssueIndex: 17,
+ Index: 1,
+ PosterID: 72,
+ PosterName: "test",
+ PosterEmail: "test@example.com",
+ CommentType: "",
+ Content: "This is a regular comment",
+ Created: now,
+ Meta: map[string]any{},
+ }, {
+ IssueIndex: 17,
+ Index: 2,
+ PosterID: 72,
+ PosterName: "test",
+ PosterEmail: "test@example.com",
+ CommentType: "pull_scheduled_merge",
+ Content: "enabled an automatic merge for abcd1234",
+ Created: now,
+ Meta: map[string]any{},
+ }, {
+ IssueIndex: 17,
+ Index: 3,
+ PosterID: 72,
+ PosterName: "test",
+ PosterEmail: "test@example.com",
+ CommentType: "change_target_branch",
+ Content: "changed target branch from `master` to `main`",
+ Created: now,
+ Meta: map[string]any{
+ "OldRef": "master",
+ "NewRef": "main",
+ },
+ }, {
+ IssueIndex: 17,
+ Index: 4,
+ PosterID: 72,
+ PosterName: "test",
+ PosterEmail: "test@example.com",
+ CommentType: "pull_cancel_scheduled_merge",
+ Content: "canceled the automatic merge",
+ Created: now,
+ Meta: map[string]any{},
+ }}
+
+ for i, note := range notes {
+ actualComment := *downloader.convertNoteToComment(17, ¬e)
+ assert.EqualValues(t, actualComment, comments[i])
+ }
+}
+
func TestGitlabIIDResolver(t *testing.T) {
r := gitlabIIDResolver{}
r.recordIssueIID(1)
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index 0b83f3b4a3..5bb3056161 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -250,14 +250,13 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba
}
log.Warn("migrating milestones is not supported, ignored")
}
-
msBatchSize := uploader.MaxBatchInsertSize("milestone")
for len(milestones) > 0 {
if len(milestones) < msBatchSize {
msBatchSize = len(milestones)
}
- if err := uploader.CreateMilestones(milestones...); err != nil {
+ if err := uploader.CreateMilestones(milestones[:msBatchSize]...); err != nil {
return err
}
milestones = milestones[msBatchSize:]
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index 33664562fe..21d5f08205 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -449,19 +449,17 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
return false
}
- var gitRepo *git.Repository
- if len(results) == 0 {
- log.Trace("SyncMirrors [repo: %-v]: no branches updated", m.Repo)
- } else {
- log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
- gitRepo, err = gitrepo.OpenRepository(ctx, m.Repo)
- if err != nil {
- log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err)
- return false
- }
- defer gitRepo.Close()
+ gitRepo, err := gitrepo.OpenRepository(ctx, m.Repo)
+ if err != nil {
+ log.Error("SyncMirrors [repo: %-v]: unable to OpenRepository: %v", m.Repo, err)
+ return false
+ }
+ defer gitRepo.Close()
+ log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
+ if len(results) > 0 {
if ok := checkAndUpdateEmptyRepository(ctx, m, gitRepo, results); !ok {
+ log.Error("SyncMirrors [repo: %-v]: checkAndUpdateEmptyRepository: %v", m.Repo, err)
return false
}
}
@@ -479,10 +477,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err)
continue
}
- objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, m.Repo)
- if err != nil {
- log.Error("SyncMirrors [repo: %-v]: unable to GetHashTypeOfRepo: %v", m.Repo, err)
- }
+ objectFormat := git.ObjectFormatFromName(m.Repo.ObjectFormatName)
notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
RefFullName: result.refName,
OldCommitID: objectFormat.EmptyObjectID().String(),
@@ -537,16 +532,24 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
}
log.Trace("SyncMirrors [repo: %-v]: done notifying updated branches/tags - now updating last commit time", m.Repo)
- // Get latest commit date and update to current repository updated time
- commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath())
+ isEmpty, err := gitRepo.IsEmpty()
if err != nil {
- log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
+ log.Error("SyncMirrors [repo: %-v]: unable to check empty git repo: %v", m.Repo, err)
return false
}
+ if !isEmpty {
+ // Get latest commit date and update to current repository updated time
+ commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath())
+ if err != nil {
+ log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
+ return false
+ }
+
+ if err = repo_model.UpdateRepositoryUpdatedTime(ctx, m.RepoID, commitDate); err != nil {
+ log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err)
+ return false
+ }
- if err = repo_model.UpdateRepositoryUpdatedTime(ctx, m.RepoID, commitDate); err != nil {
- log.Error("SyncMirrors [repo: %-v]: unable to update repository 'updated_unix': %v", m.Repo, err)
- return false
}
log.Trace("SyncMirrors [repo: %-v]: Successfully updated", m.Repo)
@@ -593,7 +596,7 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, gi
m.Repo.DefaultBranch = firstName
}
// Update the git repository default branch
- if err := gitRepo.SetDefaultBranch(m.Repo.DefaultBranch); err != nil {
+ if err := gitrepo.SetDefaultBranch(ctx, m.Repo, m.Repo.DefaultBranch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err)
desc := fmt.Sprintf("Failed to update default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err)
diff --git a/services/notify/notify.go b/services/notify/notify.go
index 16fbb6325d..0c8262ef7a 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -91,7 +91,7 @@ func AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues
// NewPullRequest notifies new pull request to notifiers
func NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) {
if err := pr.LoadIssue(ctx); err != nil {
- log.Error("%v", err)
+ log.Error("LoadIssue failed: %v", err)
return
}
if err := pr.Issue.LoadPoster(ctx); err != nil {
@@ -112,7 +112,7 @@ func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *iss
// PullRequestReview notifies new pull request review
func PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
if err := review.LoadReviewer(ctx); err != nil {
- log.Error("%v", err)
+ log.Error("LoadReviewer failed: %v", err)
return
}
for _, notifier := range notifiers {
diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go
index 104548b421..664ab34559 100644
--- a/services/packages/alpine/repository.go
+++ b/services/packages/alpine/repository.go
@@ -23,6 +23,7 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
alpine_model "code.gitea.io/gitea/models/packages/alpine"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
@@ -30,7 +31,10 @@ import (
packages_service "code.gitea.io/gitea/services/packages"
)
-const IndexFilename = "APKINDEX.tar.gz"
+const (
+ IndexFilename = "APKINDEX"
+ IndexArchiveFilename = IndexFilename + ".tar.gz"
+)
// GetOrCreateRepositoryVersion gets or creates the internal repository package
// The Alpine registry needs multiple index files which are stored in this package.
@@ -120,7 +124,22 @@ func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, branch, re
return err
}
- return buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture)
+ architectures := container.SetOf(architecture)
+ if architecture == alpine_module.NoArch {
+ // Update all other architectures too when updating the noarch index
+ additionalArchitectures, err := alpine_model.GetArchitectures(ctx, ownerID, repository)
+ if err != nil {
+ return err
+ }
+ architectures.AddMultiple(additionalArchitectures...)
+ }
+
+ for architecture := range architectures {
+ if err := buildPackagesIndex(ctx, ownerID, pv, branch, repository, architecture); err != nil {
+ return err
+ }
+ }
+ return nil
}
type packageData struct {
@@ -133,8 +152,7 @@ type packageData struct {
type packageCache = map[*packages_model.PackageFile]*packageData
-// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
-func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
+func searchPackageFiles(ctx context.Context, ownerID int64, branch, repository, architecture string) ([]*packages_model.PackageFile, error) {
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
OwnerID: ownerID,
PackageType: packages_model.TypeAlpine,
@@ -145,13 +163,30 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
alpine_module.PropertyArchitecture: architecture,
},
})
+ if err != nil {
+ return nil, err
+ }
+ return pfs, nil
+}
+
+// https://wiki.alpinelinux.org/wiki/Apk_spec#APKINDEX_Format
+func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, branch, repository, architecture string) error {
+ pfs, err := searchPackageFiles(ctx, ownerID, branch, repository, architecture)
if err != nil {
return err
}
+ if architecture != alpine_module.NoArch {
+ // Add all noarch packages too
+ noarchFiles, err := searchPackageFiles(ctx, ownerID, branch, repository, alpine_module.NoArch)
+ if err != nil {
+ return err
+ }
+ pfs = append(pfs, noarchFiles...)
+ }
// Delete the package indices if there are no packages
if len(pfs) == 0 {
- pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
+ pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s|%s", branch, repository, architecture))
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
} else if pf == nil {
@@ -206,7 +241,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
fmt.Fprintf(&buf, "C:%s\n", pd.FileMetadata.Checksum)
fmt.Fprintf(&buf, "P:%s\n", pd.Package.Name)
fmt.Fprintf(&buf, "V:%s\n", pd.Version.Version)
- fmt.Fprintf(&buf, "A:%s\n", pd.FileMetadata.Architecture)
+ fmt.Fprintf(&buf, "A:%s\n", architecture)
if pd.VersionMetadata.Description != "" {
fmt.Fprintf(&buf, "T:%s\n", pd.VersionMetadata.Description)
}
@@ -244,7 +279,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
h := sha1.New()
- if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), "APKINDEX", buf.Bytes(), true); err != nil {
+ if err := writeGzipStream(io.MultiWriter(unsignedIndexContent, h), IndexFilename, buf.Bytes(), true); err != nil {
return err
}
@@ -299,13 +334,18 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
repoVersion,
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
- Filename: IndexFilename,
+ Filename: IndexArchiveFilename,
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
},
Creator: user_model.NewGhostUser(),
Data: signedIndexContent,
IsLead: false,
OverwriteExisting: true,
+ Properties: map[string]string{
+ alpine_module.PropertyBranch: branch,
+ alpine_module.PropertyRepository: repository,
+ alpine_module.PropertyArchitecture: architecture,
+ },
},
)
return err
diff --git a/services/packages/auth.go b/services/packages/auth.go
index 2f78b26f50..8263c28bed 100644
--- a/services/packages/auth.go
+++ b/services/packages/auth.go
@@ -33,7 +33,7 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- tokenString, err := token.SignedString([]byte(setting.SecretKey))
+ tokenString, err := token.SignedString(setting.GetGeneralTokenSigningSecret())
if err != nil {
return "", err
}
@@ -57,7 +57,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
- return []byte(setting.SecretKey), nil
+ return setting.GetGeneralTokenSigningSecret(), nil
})
if err != nil {
return 0, err
diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go
index 0ff8077bc9..5d5120c6a0 100644
--- a/services/packages/cleanup/cleanup.go
+++ b/services/packages/cleanup/cleanup.go
@@ -12,8 +12,8 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
- "code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
alpine_service "code.gitea.io/gitea/services/packages/alpine"
cargo_service "code.gitea.io/gitea/services/packages/cargo"
@@ -60,7 +60,7 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
for _, p := range packages {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
PackageID: p.ID,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go
index dd3f158dbf..3f5f43bbc0 100644
--- a/services/packages/container/cleanup.go
+++ b/services/packages/container/cleanup.go
@@ -9,8 +9,8 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
+ "code.gitea.io/gitea/modules/optional"
container_module "code.gitea.io/gitea/modules/packages/container"
- "code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
digest "github.com/opencontainers/go-digest"
@@ -59,8 +59,8 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
ExactMatch: true,
Value: container_model.UploadVersion,
},
- IsInternal: util.OptionalBoolTrue,
- HasFiles: util.OptionalBoolFalse,
+ IsInternal: optional.Some(true),
+ HasFiles: optional.Some(false),
})
if err != nil {
return err
diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go
index 86c54e40c8..611faa6ade 100644
--- a/services/packages/debian/repository.go
+++ b/services/packages/debian/repository.go
@@ -342,7 +342,7 @@ func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages
fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
- fmt.Fprint(w, "Acquire-By-Hash: yes")
+ fmt.Fprint(w, "Acquire-By-Hash: yes\n")
pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
if err != nil {
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 56d5cc04de..64b1ddd869 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -18,10 +18,10 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
)
@@ -330,7 +330,7 @@ func CheckCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User)
if setting.Packages.LimitTotalOwnerCount > -1 {
totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: owner.ID,
- IsInternal: util.OptionalBoolFalse,
+ IsInternal: optional.Some(false),
})
if err != nil {
log.Error("CountVersions failed: %v", err)
@@ -640,7 +640,7 @@ func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
Page: 1,
},
OwnerID: userID,
- IsInternal: util.OptionalBoolNone,
+ IsInternal: optional.None[bool](),
})
if err != nil {
return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
diff --git a/services/pull/check.go b/services/pull/check.go
index dd6c3ed230..f4dd332b14 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -222,10 +222,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com
}
defer gitRepo.Close()
- objectFormat, err := gitRepo.GetObjectFormat()
- if err != nil {
- return nil, fmt.Errorf("%-v GetObjectFormat: %w", pr.BaseRepo, err)
- }
+ objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
// Get the commit from BaseBranch where the pull request got merged
mergeCommit, _, err := git.NewCommand(ctx, "rev-list", "--ancestry-path", "--merges", "--reverse").
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index a602ddf106..c4b4709fe3 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -34,9 +34,9 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
}
}
- for _, commitStatus := range commitStatuses {
+ for _, gp := range requiredContextsGlob {
var targetStatus structs.CommitStatusState
- for _, gp := range requiredContextsGlob {
+ for _, commitStatus := range commitStatuses {
if gp.Match(commitStatus.Context) {
targetStatus = commitStatus.State
matchedCount++
@@ -44,13 +44,21 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
}
}
- if targetStatus != "" && targetStatus.NoBetterThan(returnedStatus) {
+ // If required rule not match any action, then it is pending
+ if targetStatus == "" {
+ if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
+ returnedStatus = structs.CommitStatusPending
+ }
+ break
+ }
+
+ if targetStatus.NoBetterThan(returnedStatus) {
returnedStatus = targetStatus
}
}
}
- if matchedCount == 0 {
+ if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess {
status := git_model.CalcCommitStatus(commitStatuses)
if status != nil {
return status.State
@@ -143,7 +151,7 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
return "", errors.Wrap(err, "LoadBaseRepo")
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
if err != nil {
return "", errors.Wrap(err, "GetLatestCommitStatus")
}
diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go
new file mode 100644
index 0000000000..592acdd55c
--- /dev/null
+++ b/services/pull/commit_status_test.go
@@ -0,0 +1,65 @@
+// Copyright 2024 The Gitea Authors.
+// All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+ "testing"
+
+ git_model "code.gitea.io/gitea/models/git"
+ "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMergeRequiredContextsCommitStatus(t *testing.T) {
+ testCases := [][]*git_model.CommitStatus{
+ {
+ {Context: "Build 1", State: structs.CommitStatusSuccess},
+ {Context: "Build 2", State: structs.CommitStatusSuccess},
+ {Context: "Build 3", State: structs.CommitStatusSuccess},
+ },
+ {
+ {Context: "Build 1", State: structs.CommitStatusSuccess},
+ {Context: "Build 2", State: structs.CommitStatusSuccess},
+ {Context: "Build 2t", State: structs.CommitStatusPending},
+ },
+ {
+ {Context: "Build 1", State: structs.CommitStatusSuccess},
+ {Context: "Build 2", State: structs.CommitStatusSuccess},
+ {Context: "Build 2t", State: structs.CommitStatusFailure},
+ },
+ {
+ {Context: "Build 1", State: structs.CommitStatusSuccess},
+ {Context: "Build 2", State: structs.CommitStatusSuccess},
+ {Context: "Build 2t", State: structs.CommitStatusSuccess},
+ },
+ {
+ {Context: "Build 1", State: structs.CommitStatusSuccess},
+ {Context: "Build 2", State: structs.CommitStatusSuccess},
+ {Context: "Build 2t", State: structs.CommitStatusSuccess},
+ },
+ }
+ testCasesRequiredContexts := [][]string{
+ {"Build*"},
+ {"Build*", "Build 2t*"},
+ {"Build*", "Build 2t*"},
+ {"Build*", "Build 2t*", "Build 3*"},
+ {"Build*", "Build *", "Build 2t*", "Build 1*"},
+ }
+
+ testCasesExpected := []structs.CommitStatusState{
+ structs.CommitStatusSuccess,
+ structs.CommitStatusPending,
+ structs.CommitStatusFailure,
+ structs.CommitStatusPending,
+ structs.CommitStatusSuccess,
+ }
+
+ for i, commitStatuses := range testCases {
+ if MergeRequiredContextsCommitStatus(commitStatuses, testCasesRequiredContexts[i]) != testCasesExpected[i] {
+ assert.Fail(t, "Test case failed", "Test case %d failed", i+1)
+ }
+ }
+}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 63f0268beb..e37540a96f 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -267,6 +267,10 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
if err := doMergeStyleSquash(mergeCtx, message); err != nil {
return "", err
}
+ case repo_model.MergeStyleFastForwardOnly:
+ if err := doMergeStyleFastForwardOnly(mergeCtx); err != nil {
+ return "", err
+ }
default:
return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
}
@@ -377,6 +381,13 @@ func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *g
StdErr: ctx.errbuf.String(),
Err: err,
}
+ } else if mergeStyle == repo_model.MergeStyleFastForwardOnly && strings.Contains(ctx.errbuf.String(), "Not possible to fast-forward, aborting") {
+ log.Debug("MergeDivergingFastForwardOnly %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
+ return models.ErrMergeDivergingFastForwardOnly{
+ StdOut: ctx.outbuf.String(),
+ StdErr: ctx.errbuf.String(),
+ Err: err,
+ }
}
log.Error("git merge %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
return fmt.Errorf("git merge %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
@@ -486,7 +497,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged}
}
- objectFormat, _ := baseGitRepo.GetObjectFormat()
+ objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if len(commitID) != objectFormat.FullLength() {
return fmt.Errorf("Wrong commit ID")
}
diff --git a/services/pull/merge_ff_only.go b/services/pull/merge_ff_only.go
new file mode 100644
index 0000000000..f57c732104
--- /dev/null
+++ b/services/pull/merge_ff_only.go
@@ -0,0 +1,21 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+)
+
+// doMergeStyleFastForwardOnly merges the tracking into the current HEAD - which is assumed to be staging branch (equal to the pr.BaseBranch)
+func doMergeStyleFastForwardOnly(ctx *mergeContext) error {
+ cmd := git.NewCommand(ctx, "merge", "--ff-only").AddDynamicArguments(trackingBranch)
+ if err := runMergeCommand(ctx, repo_model.MergeStyleFastForwardOnly, cmd); err != nil {
+ log.Error("%-v Unable to merge tracking into base: %v", ctx.pr, err)
+ return err
+ }
+
+ return nil
+}
diff --git a/services/pull/merge_merge.go b/services/pull/merge_merge.go
index 0f7664297a..bf56c071db 100644
--- a/services/pull/merge_merge.go
+++ b/services/pull/merge_merge.go
@@ -9,7 +9,7 @@ import (
"code.gitea.io/gitea/modules/log"
)
-// doMergeStyleMerge merges the tracking into the current HEAD - which is assumed to tbe staging branch (equal to the pr.BaseBranch)
+// doMergeStyleMerge merges the tracking branch into the current HEAD - which is assumed to be the staging branch (equal to the pr.BaseBranch)
func doMergeStyleMerge(ctx *mergeContext, message string) error {
cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch)
if err := runMergeCommand(ctx, repo_model.MergeStyleMerge, cmd); err != nil {
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 1182a75c89..80eaf67341 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -21,7 +21,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
- gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
@@ -31,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/sync"
"code.gitea.io/gitea/modules/util"
+ gitea_context "code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
)
@@ -40,6 +40,14 @@ var pullWorkingPool = sync.NewExclusivePool()
// NewPullRequest creates new pull request with labels for repository.
func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
+ if err := issue.LoadPoster(ctx); err != nil {
+ return err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, issue.Poster, repo.OwnerID) || user_model.IsUserBlockedBy(ctx, issue.Poster, assigneeIDs...) {
+ return user_model.ErrBlockedUser
+ }
+
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil {
if !git_model.IsErrBranchNotExist(err) {
@@ -69,6 +77,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
}
defer baseGitRepo.Close()
+ var reviewNotifers []*issue_service.ReviewRequestNotifier
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
return err
@@ -128,7 +137,8 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
}
if !pr.IsWorkInProgress(ctx) {
- if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, pr); err != nil {
+ reviewNotifers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
+ if err != nil {
return err
}
}
@@ -142,11 +152,12 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
}
baseGitRepo.Close() // close immediately to avoid notifications will open the repository again
+ issue_service.ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers)
+
mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
if err != nil {
return err
}
-
notify_service.NewPullRequest(ctx, pr, mentions)
if len(issue.Labels) > 0 {
notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
@@ -329,7 +340,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
}
if err == nil {
for _, pr := range prs {
- objectFormat, _ := gitrepo.GetObjectFormatOfRepo(ctx, pr.BaseRepo)
+ objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
if err != nil {
@@ -515,6 +526,25 @@ func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, pre
return nil
}
+// UpdatePullsRefs update all the PRs head file pointers like /refs/pull/1/head so that it will be dependent by other operations
+func UpdatePullsRefs(ctx context.Context, repo *repo_model.Repository, update *repo_module.PushUpdateOptions) {
+ branch := update.RefFullName.BranchName()
+ // GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
+ prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch)
+ if err != nil {
+ log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repo.ID, branch, err)
+ } else {
+ for _, pr := range prs {
+ log.Trace("Updating PR[%d]: composing new test task", pr.ID)
+ if pr.Flow == issues_model.PullRequestFlowGithub {
+ if err := PushToBaseRepo(ctx, pr); err != nil {
+ log.Error("PushToBaseRepo: %v", err)
+ }
+ }
+ }
+ }
+}
+
// UpdateRef update refs/pull/id/head directly for agit flow pull request
func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
@@ -872,7 +902,7 @@ func getAllCommitStatus(ctx context.Context, gitRepo *git.Repository, pr *issues
return nil, nil, shaErr
}
- statuses, _, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{ListAll: true})
+ statuses, _, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll)
lastStatus = git_model.CalcCommitStatus(statuses)
return statuses, lastStatus, err
}
@@ -959,12 +989,12 @@ func GetPullCommits(ctx *gitea_context.Context, issue *issues_model.Issue) ([]Co
for _, commit := range prInfo.Commits {
var committerOrAuthorName string
var commitTime time.Time
- if commit.Committer != nil {
- committerOrAuthorName = commit.Committer.Name
- commitTime = commit.Committer.When
- } else {
+ if commit.Author != nil {
committerOrAuthorName = commit.Author.Name
commitTime = commit.Author.When
+ } else {
+ committerOrAuthorName = commit.Committer.Name
+ commitTime = commit.Committer.When
}
commits = append(commits, CommitInfo{
diff --git a/services/pull/review.go b/services/pull/review.go
index d4ea975612..5bf1991d13 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
@@ -25,6 +26,23 @@ import (
var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
+// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR.
+type ErrDismissRequestOnClosedPR struct{}
+
+// IsErrDismissRequestOnClosedPR checks if an error is an ErrDismissRequestOnClosedPR.
+func IsErrDismissRequestOnClosedPR(err error) bool {
+ _, ok := err.(ErrDismissRequestOnClosedPR)
+ return ok
+}
+
+func (err ErrDismissRequestOnClosedPR) Error() string {
+ return "can't dismiss a review associated to a closed or merged PR"
+}
+
+func (err ErrDismissRequestOnClosedPR) Unwrap() error {
+ return util.ErrPermissionDenied
+}
+
// checkInvalidation checks if the line of code comment got changed by another commit.
// If the line got changed the comment is going to be invalidated.
func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
@@ -52,11 +70,9 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
issueIDs := prs.GetIssueIDs()
codeComments, err := db.Find[issues_model.Comment](ctx, issues_model.FindCommentsOptions{
- ListOptions: db.ListOptions{
- ListAll: true,
- },
+ ListOptions: db.ListOptionsAll,
Type: issues_model.CommentTypeCode,
- Invalidated: util.OptionalBoolFalse,
+ Invalidated: optional.Some(false),
IssueIDs: issueIDs,
})
if err != nil {
@@ -71,7 +87,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
}
// CreateCodeComment creates a comment on the code line
-func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string) (*issues_model.Comment, error) {
+func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) {
var (
existsReview bool
err error
@@ -104,6 +120,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
treePath,
line,
replyReviewID,
+ attachments,
)
if err != nil {
return nil, err
@@ -144,6 +161,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
treePath,
line,
review.ID,
+ attachments,
)
if err != nil {
return nil, err
@@ -162,7 +180,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
}
// createCodeComment creates a plain code comment at the specified line / path
-func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) {
+func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64, attachments []string) (*issues_model.Comment, error) {
var commitID, patch string
if err := issue.LoadPullRequest(ctx); err != nil {
return nil, fmt.Errorf("LoadPullRequest: %w", err)
@@ -260,16 +278,17 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
ReviewID: reviewID,
Patch: patch,
Invalidated: invalidated,
+ Attachments: attachments,
})
}
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) {
- pr, err := issue.GetPullRequest(ctx)
- if err != nil {
+ if err := issue.LoadPullRequest(ctx); err != nil {
return nil, nil, err
}
+ pr := issue.PullRequest
var stale bool
if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
stale = false
@@ -319,12 +338,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
// DismissApprovalReviews dismiss all approval reviews because of new commits
func DismissApprovalReviews(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
- ListOptions: db.ListOptions{
- ListAll: true,
- },
- IssueID: pull.IssueID,
- Type: issues_model.ReviewTypeApprove,
- Dismissed: util.OptionalBoolFalse,
+ ListOptions: db.ListOptionsAll,
+ IssueID: pull.IssueID,
+ Type: issues_model.ReviewTypeApprove,
+ Dismissed: optional.Some(false),
})
if err != nil {
return err
@@ -383,6 +400,21 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
return nil, fmt.Errorf("reviews's repository is not the same as the one we expect")
}
+ issue := review.Issue
+
+ if issue.IsClosed {
+ return nil, ErrDismissRequestOnClosedPR{}
+ }
+
+ if issue.IsPull {
+ if err := issue.LoadPullRequest(ctx); err != nil {
+ return nil, err
+ }
+ if issue.PullRequest.HasMerged {
+ return nil, ErrDismissRequestOnClosedPR{}
+ }
+ }
+
if err := issues_model.DismissReview(ctx, review, isDismiss); err != nil {
return nil, err
}
@@ -391,7 +423,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
reviews, err := issues_model.FindReviews(ctx, issues_model.FindReviewOptions{
IssueID: review.IssueID,
ReviewerID: review.ReviewerID,
- Dismissed: util.OptionalBoolFalse,
+ Dismissed: optional.Some(false),
})
if err != nil {
return nil, err
diff --git a/services/pull/review_test.go b/services/pull/review_test.go
new file mode 100644
index 0000000000..3bce1e523d
--- /dev/null
+++ b/services/pull/review_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull_test
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ pull_service "code.gitea.io/gitea/services/pull"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDismissReview(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{})
+ assert.NoError(t, pull.LoadIssue(db.DefaultContext))
+ issue := pull.Issue
+ assert.NoError(t, issue.LoadRepo(db.DefaultContext))
+ reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
+ Issue: issue,
+ Reviewer: reviewer,
+ Type: issues_model.ReviewTypeReject,
+ })
+
+ assert.NoError(t, err)
+ issue.IsClosed = true
+ pull.HasMerged = false
+ assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
+ assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+ _, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
+ assert.Error(t, err)
+ assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
+
+ pull.HasMerged = true
+ pull.Issue.IsClosed = false
+ assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "is_closed"))
+ assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged"))
+ _, err = pull_service.DismissReview(db.DefaultContext, review.ID, issue.RepoID, "", &user_model.User{}, false, false)
+ assert.Error(t, err)
+ assert.True(t, pull_service.IsErrDismissRequestOnClosedPR(err))
+}
diff --git a/services/release/release.go b/services/release/release.go
index e3b18d1f2b..ba5fd1dd98 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -88,7 +88,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
created = true
rel.LowerTagName = strings.ToLower(rel.TagName)
- objectFormat, _ := gitRepo.GetObjectFormat()
+ objectFormat := git.ObjectFormatFromName(rel.Repo.ObjectFormatName)
commits := repository.NewPushCommits()
commits.HeadCommit = repository.CommitToPushCommit(commit)
commits.CompareURL = rel.Repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), commit.ID.String())
@@ -326,10 +326,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
}
refName := git.RefNameFromTag(rel.TagName)
- objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, repo)
- if err != nil {
- return err
- }
+ objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
notify_service.PushCommits(
ctx, doer, repo,
&repository.PushUpdateOptions{
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 29517114a2..d57d6ea6d4 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -126,35 +127,26 @@ func adoptRepository(ctx context.Context, u *user_model.User, repo *repo_model.R
repo.IsEmpty = false
- // Don't bother looking this repo in the context it won't be there
- gitRepo, err := gitrepo.OpenRepository(ctx, repo)
- if err != nil {
- return fmt.Errorf("openRepository: %w", err)
- }
- defer gitRepo.Close()
-
if len(defaultBranch) > 0 {
repo.DefaultBranch = defaultBranch
- if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err)
}
} else {
- repo.DefaultBranch, err = gitRepo.GetDefaultBranch()
+ repo.DefaultBranch, err = gitrepo.GetDefaultBranch(ctx, repo)
if err != nil {
repo.DefaultBranch = setting.Repository.DefaultBranch
- if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err)
}
}
}
branches, _ := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
- RepoID: repo.ID,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
- IsDeletedBranch: util.OptionalBoolFalse,
+ RepoID: repo.ID,
+ ListOptions: db.ListOptionsAll,
+ IsDeletedBranch: optional.Some(false),
})
found := false
@@ -187,7 +179,7 @@ func adoptRepository(ctx context.Context, u *user_model.User, repo *repo_model.R
repo.DefaultBranch = setting.Repository.DefaultBranch
}
- if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err)
}
}
@@ -196,6 +188,13 @@ func adoptRepository(ctx context.Context, u *user_model.User, repo *repo_model.R
return fmt.Errorf("updateRepository: %w", err)
}
+ // Don't bother looking this repo in the context it won't be there
+ gitRepo, err := gitrepo.OpenRepository(ctx, repo)
+ if err != nil {
+ return fmt.Errorf("openRepository: %w", err)
+ }
+ defer gitRepo.Close()
+
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
return fmt.Errorf("SyncReleasesWithTags: %w", err)
}
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index 5deec259da..ec6e9dfac3 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -10,7 +10,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
+ "code.gitea.io/gitea/services/contexttest"
_ "code.gitea.io/gitea/models/actions"
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 9ad8689ea3..229ac54f30 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -16,16 +16,19 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
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/gitrepo"
"code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/queue"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
notify_service "code.gitea.io/gitea/services/notify"
+ files_service "code.gitea.io/gitea/services/repository/files"
"xorm.io/builder"
)
@@ -52,7 +55,7 @@ type Branch struct {
}
// LoadBranches loads branches from the repository limited by page & pageSize.
-func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch util.OptionalBool, keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
+func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) {
defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch)
if err != nil {
return nil, nil, 0, err
@@ -98,7 +101,6 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
if err != nil {
return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
}
-
branches = append(branches, branch)
}
@@ -108,10 +110,44 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git
if err != nil {
return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err)
}
-
return defaultBranch, branches, totalNumOfBranches, nil
}
+func getDivergenceCacheKey(repoID int64, branchName string) string {
+ return fmt.Sprintf("%d-%s", repoID, branchName)
+}
+
+// getDivergenceFromCache gets the divergence from cache
+func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) {
+ data := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName))
+ res := git.DivergeObject{
+ Ahead: -1,
+ Behind: -1,
+ }
+ s, ok := data.([]byte)
+ if !ok || len(s) == 0 {
+ return &res, false
+ }
+
+ if err := json.Unmarshal(s, &res); err != nil {
+ log.Error("json.UnMarshal failed: %v", err)
+ return &res, false
+ }
+ return &res, true
+}
+
+func putDivergenceFromCache(repoID int64, branchName string, divergence *git.DivergeObject) error {
+ bs, err := json.Marshal(divergence)
+ if err != nil {
+ return err
+ }
+ return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), bs, 30*24*60*60)
+}
+
+func DelDivergenceFromCache(repoID int64, branchName string) error {
+ return cache.GetCache().Delete(getDivergenceCacheKey(repoID, branchName))
+}
+
func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules,
repoIDToRepo map[int64]*repo_model.Repository,
repoIDToGitRepo map[int64]*git.Repository,
@@ -122,20 +158,30 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
p := protectedBranches.GetFirstMatched(branchName)
isProtected := p != nil
- divergence := &git.DivergeObject{
- Ahead: -1,
- Behind: -1,
- }
+ var divergence *git.DivergeObject
// it's not default branch
if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted {
- var err error
- divergence, err = gitrepo.CountDivergingCommits(ctx, repo, repo.DefaultBranch, git.BranchPrefix+branchName)
- if err != nil {
- log.Error("CountDivergingCommits: %v", err)
+ var cached bool
+ divergence, cached = getDivergenceFromCache(repo.ID, dbBranch.Name)
+ if !cached {
+ var err error
+ divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName)
+ if err != nil {
+ log.Error("CountDivergingCommits: %v", err)
+ } else {
+ if err = putDivergenceFromCache(repo.ID, dbBranch.Name, divergence); err != nil {
+ log.Error("putDivergenceFromCache: %v", err)
+ }
+ }
}
}
+ if divergence == nil {
+ // tolerate the error that we cannot get divergence
+ divergence = &git.DivergeObject{Ahead: -1, Behind: -1}
+ }
+
pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx, repo.ID, branchName)
if err != nil {
return nil, fmt.Errorf("GetLatestPullRequestByHeadInfo: %v", err)
@@ -220,44 +266,91 @@ func checkBranchName(ctx context.Context, repo *repo_model.Repository, name stri
return err
}
-// syncBranchToDB sync the branch information in the database. It will try to update the branch first,
-// if updated success with affect records > 0, then all are done. Because that means the branch has been in the database.
-// If no record is affected, that means the branch does not exist in database. So there are two possibilities.
-// One is this is a new branch, then we just need to insert the record. Another is the branches haven't been synced,
-// then we need to sync all the branches into database.
-func syncBranchToDB(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
- cnt, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit)
- if err != nil {
- return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
- }
- if cnt > 0 { // This means branch does exist, so it's a normal update. It also means the branch has been synced.
- return nil
+// SyncBranchesToDB sync the branch information in the database.
+// It will check whether the branches of the repository have never been synced before.
+// If so, it will sync all branches of the repository.
+// Otherwise, it will sync the branches that need to be updated.
+func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, commitIDs []string, getCommit func(commitID string) (*git.Commit, error)) error {
+ // Some designs that make the code look strange but are made for performance optimization purposes:
+ // 1. Sync branches in a batch to reduce the number of DB queries.
+ // 2. Lazy load commit information since it may be not necessary.
+ // 3. Exit early if synced all branches of git repo when there's no branch in DB.
+ // 4. Check the branches in DB if they are already synced.
+ //
+ // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
+ // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
+ // For the first batch, it will hit optimization 3.
+ // For other batches, it will hit optimization 4.
+
+ if len(branchNames) != len(commitIDs) {
+ return fmt.Errorf("branchNames and commitIDs length not match")
}
- // if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
- // we cannot simply insert the branch but need to check we have branches or not
- hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
- RepoID: repoID,
- IsDeletedBranch: util.OptionalBoolFalse,
- }.ToConds())
- if err != nil {
- return err
- }
- if !hasBranch {
- if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
- return fmt.Errorf("repo_module.SyncRepoBranches %d:%s failed: %v", repoID, branchName, err)
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ branches, err := git_model.GetBranches(ctx, repoID, branchNames)
+ if err != nil {
+ return fmt.Errorf("git_model.GetBranches: %v", err)
+ }
+
+ if len(branches) == 0 {
+ // if user haven't visit UI but directly push to a branch after upgrading from 1.20 -> 1.21,
+ // we cannot simply insert the branch but need to check we have branches or not
+ hasBranch, err := db.Exist[git_model.Branch](ctx, git_model.FindBranchOptions{
+ RepoID: repoID,
+ IsDeletedBranch: optional.Some(false),
+ }.ToConds())
+ if err != nil {
+ return err
+ }
+ if !hasBranch {
+ if _, err = repo_module.SyncRepoBranches(ctx, repoID, pusherID); err != nil {
+ return fmt.Errorf("repo_module.SyncRepoBranches %d failed: %v", repoID, err)
+ }
+ return nil
+ }
+ }
+
+ branchMap := make(map[string]*git_model.Branch, len(branches))
+ for _, branch := range branches {
+ branchMap[branch.Name] = branch
+ }
+
+ newBranches := make([]*git_model.Branch, 0, len(branchNames))
+
+ for i, branchName := range branchNames {
+ commitID := commitIDs[i]
+ branch, exist := branchMap[branchName]
+ if exist && branch.CommitID == commitID && !branch.IsDeleted {
+ continue
+ }
+
+ commit, err := getCommit(commitID)
+ if err != nil {
+ return fmt.Errorf("get commit of %s failed: %v", branchName, err)
+ }
+
+ if exist {
+ if _, err := git_model.UpdateBranch(ctx, repoID, pusherID, branchName, commit); err != nil {
+ return fmt.Errorf("git_model.UpdateBranch %d:%s failed: %v", repoID, branchName, err)
+ }
+ return nil
+ }
+
+ // if database have branches but not this branch, it means this is a new branch
+ newBranches = append(newBranches, &git_model.Branch{
+ RepoID: repoID,
+ Name: branchName,
+ CommitID: commit.ID.String(),
+ CommitMessage: commit.Summary(),
+ PusherID: pusherID,
+ CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
+ })
+ }
+
+ if len(newBranches) > 0 {
+ return db.Insert(ctx, newBranches)
}
return nil
- }
-
- // if database have branches but not this branch, it means this is a new branch
- return db.Insert(ctx, &git_model.Branch{
- RepoID: repoID,
- Name: branchName,
- CommitID: commit.ID.String(),
- CommitMessage: commit.Summary(),
- PusherID: pusherID,
- CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
})
}
@@ -317,17 +410,17 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
log.Error("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
- if err := actions_model.CancelRunningJobs(
+ if err := actions_model.CancelPreviousJobs(
ctx,
repo.ID,
from,
"",
webhook_module.HookEventSchedule,
); err != nil {
- log.Error("CancelRunningJobs: %v", err)
+ log.Error("CancelPreviousJobs: %v", err)
}
- err2 = gitRepo.SetDefaultBranch(to)
+ err2 = gitrepo.SetDefaultBranch(ctx, repo, to)
if err2 != nil {
return err2
}
@@ -378,11 +471,6 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
return fmt.Errorf("GetBranch: %vc", err)
}
- objectFormat, err := gitRepo.GetObjectFormat()
- if err != nil {
- return err
- }
-
if rawBranch.IsDeleted {
return nil
}
@@ -404,6 +492,8 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
return err
}
+ objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
// Don't return error below this
if err := PushUpdate(
&repo_module.PushUpdateOptions{
@@ -485,17 +575,17 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
log.Error("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
- if err := actions_model.CancelRunningJobs(
+ if err := actions_model.CancelPreviousJobs(
ctx,
repo.ID,
oldDefaultBranchName,
"",
webhook_module.HookEventSchedule,
); err != nil {
- log.Error("CancelRunningJobs: %v", err)
+ log.Error("CancelPreviousJobs: %v", err)
}
- if err := gitRepo.SetDefaultBranch(newBranchName); err != nil {
+ if err := gitrepo.SetDefaultBranch(ctx, repo, newBranchName); err != nil {
if !git.IsErrUnsupportedVersion(err) {
return err
}
diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go
index dccc124748..4a43ae2a28 100644
--- a/services/repository/collaboration.go
+++ b/services/repository/collaboration.go
@@ -11,13 +11,14 @@ import (
"code.gitea.io/gitea/models/db"
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"
)
// DeleteCollaboration removes collaboration relation between the user and repository.
-func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
+func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, collaborator *user_model.User) (err error) {
collaboration := &repo_model.Collaboration{
RepoID: repo.ID,
- UserID: uid,
+ UserID: collaborator.ID,
}
ctx, committer, err := db.TxContext(ctx)
@@ -31,20 +32,25 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, uid i
} else if has == 0 {
return committer.Commit()
}
+
+ if err := repo.LoadOwner(ctx); err != nil {
+ return err
+ }
+
if err = access_model.RecalculateAccesses(ctx, repo); err != nil {
return err
}
- if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil {
+ if err = repo_model.WatchRepo(ctx, collaborator, repo, false); err != nil {
return err
}
- if err = models.ReconsiderWatches(ctx, repo, uid); err != nil {
+ if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
return err
}
// Unassign a user from any issue (s)he has been assigned to in the repository
- if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil {
+ if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
return err
}
diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go
index c3d006bfd8..a2eb06b81a 100644
--- a/services/repository/collaboration_test.go
+++ b/services/repository/collaboration_test.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
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"
)
@@ -16,13 +17,15 @@ import (
func TestRepository_DeleteCollaboration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
- assert.NoError(t, repo.LoadOwner(db.DefaultContext))
- assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
- unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
- assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, 4))
- unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
+ assert.NoError(t, repo.LoadOwner(db.DefaultContext))
+ assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+ unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
+
+ assert.NoError(t, DeleteCollaboration(db.DefaultContext, repo, user))
+ unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: user.ID})
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID})
}
diff --git a/services/repository/commit.go b/services/repository/commit.go
index 2497910a83..e8c0262ef4 100644
--- a/services/repository/commit.go
+++ b/services/repository/commit.go
@@ -7,8 +7,8 @@ import (
"context"
"fmt"
- gitea_ctx "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
+ gitea_ctx "code.gitea.io/gitea/services/context"
)
type ContainedLinks struct { // TODO: better name?
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
new file mode 100644
index 0000000000..7c1c6c2609
--- /dev/null
+++ b/services/repository/commitstatus/commitstatus.go
@@ -0,0 +1,199 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package commitstatus
+
+import (
+ "context"
+ "crypto/sha256"
+ "fmt"
+ "slices"
+
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ 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/gitrepo"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/automerge"
+)
+
+func getCacheKey(repoID int64, brancheName string) string {
+ hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", repoID, brancheName)))
+ return fmt.Sprintf("commit_status:%x", hashBytes)
+}
+
+type commitStatusCacheValue struct {
+ State string `json:"state"`
+ TargetURL string `json:"target_url"`
+}
+
+func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue {
+ c := cache.GetCache()
+ statusStr, ok := c.Get(getCacheKey(repoID, branchName)).(string)
+ if ok && statusStr != "" {
+ var cv commitStatusCacheValue
+ err := json.Unmarshal([]byte(statusStr), &cv)
+ if err == nil && cv.State != "" {
+ return &cv
+ }
+ if err != nil {
+ log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
+ }
+ }
+ return nil
+}
+
+func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error {
+ c := cache.GetCache()
+ bs, err := json.Marshal(commitStatusCacheValue{
+ State: state.String(),
+ TargetURL: targetURL,
+ })
+ if err != nil {
+ log.Warn("updateCommitStatusCache: json.Marshal failed: %v", err)
+ return nil
+ }
+ return c.Put(getCacheKey(repoID, branchName), string(bs), 3*24*60)
+}
+
+func deleteCommitStatusCache(repoID int64, branchName string) error {
+ c := cache.GetCache()
+ return c.Delete(getCacheKey(repoID, branchName))
+}
+
+// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
+// NOTE: All text-values will be trimmed from whitespaces.
+// Requires: Repo, Creator, SHA
+func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
+ repoPath := repo.RepoPath()
+
+ // confirm that commit is exist
+ gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+ if err != nil {
+ return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
+ }
+ defer closer.Close()
+
+ objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
+
+ commit, err := gitRepo.GetCommit(sha)
+ if err != nil {
+ return fmt.Errorf("GetCommit[%s]: %w", sha, err)
+ }
+ if len(sha) != objectFormat.FullLength() {
+ // use complete commit sha
+ sha = commit.ID.String()
+ }
+
+ if err := db.WithTx(ctx, func(ctx context.Context) error {
+ if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
+ Repo: repo,
+ Creator: creator,
+ SHA: commit.ID,
+ CommitStatus: status,
+ }); err != nil {
+ return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+ }
+
+ return git_model.UpdateCommitStatusSummary(ctx, repo.ID, commit.ID.String())
+ }); err != nil {
+ return err
+ }
+
+ defaultBranchCommit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
+ if err != nil {
+ return fmt.Errorf("GetBranchCommit[%s]: %w", repo.DefaultBranch, err)
+ }
+
+ if commit.ID.String() == defaultBranchCommit.ID.String() { // since one commit status updated, the combined commit status should be invalid
+ if err := deleteCommitStatusCache(repo.ID, repo.DefaultBranch); err != nil {
+ log.Error("deleteCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+ }
+ }
+
+ if status.State.IsSuccess() {
+ if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
+ return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+ }
+ }
+
+ return nil
+}
+
+// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
+func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
+ results := make([]*git_model.CommitStatus, len(repos))
+ for i, repo := range repos {
+ if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
+ results[i] = &git_model.CommitStatus{
+ State: api.CommitStatusState(cv.State),
+ TargetURL: cv.TargetURL,
+ }
+ }
+ }
+
+ // collect the latest commit of each repo
+ // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
+ repoBranchNames := make(map[int64]string, len(repos))
+ for i, repo := range repos {
+ if results[i] == nil {
+ repoBranchNames[repo.ID] = repo.DefaultBranch
+ }
+ }
+
+ repoIDsToLatestCommitSHAs, err := git_model.FindBranchesByRepoAndBranchName(ctx, repoBranchNames)
+ if err != nil {
+ return nil, fmt.Errorf("FindBranchesByRepoAndBranchName: %v", err)
+ }
+
+ var repoSHAs []git_model.RepoSHA
+ for id, sha := range repoIDsToLatestCommitSHAs {
+ repoSHAs = append(repoSHAs, git_model.RepoSHA{RepoID: id, SHA: sha})
+ }
+
+ summaryResults, err := git_model.GetLatestCommitStatusForRepoAndSHAs(ctx, repoSHAs)
+ if err != nil {
+ return nil, fmt.Errorf("GetLatestCommitStatusForRepoAndSHAs: %v", err)
+ }
+
+ for _, summary := range summaryResults {
+ for i, repo := range repos {
+ if repo.ID == summary.RepoID {
+ results[i] = summary
+ _ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
+ return repoSHA.RepoID == repo.ID
+ })
+ if results[i].State != "" {
+ if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
+ log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+ }
+ }
+ break
+ }
+ }
+ }
+
+ // call the database O(1) times to get the commit statuses for all repos
+ repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
+ if err != nil {
+ return nil, fmt.Errorf("GetLatestCommitStatusForPairs: %v", err)
+ }
+
+ for i, repo := range repos {
+ if results[i] == nil {
+ results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
+ if results[i].State != "" {
+ if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
+ log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
+ }
+ }
+ }
+ }
+
+ return results, nil
+}
diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
new file mode 100644
index 0000000000..7c9f535ae0
--- /dev/null
+++ b/services/repository/contributors_graph.go
@@ -0,0 +1,317 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "bufio"
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "code.gitea.io/gitea/models/avatars"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "gitea.com/go-chi/cache"
+)
+
+const (
+ contributorStatsCacheKey = "GetContributorStats/%s/%s"
+ contributorStatsCacheTimeout int64 = 60 * 10
+)
+
+var (
+ ErrAwaitGeneration = errors.New("generation took longer than ")
+ awaitGenerationTime = time.Second * 5
+ generateLock = sync.Map{}
+)
+
+type WeekData struct {
+ Week int64 `json:"week"` // Starting day of the week as Unix timestamp
+ Additions int `json:"additions"` // Number of additions in that week
+ Deletions int `json:"deletions"` // Number of deletions in that week
+ Commits int `json:"commits"` // Number of commits in that week
+}
+
+// ContributorData represents statistical git commit count data
+type ContributorData struct {
+ Name string `json:"name"` // Display name of the contributor
+ Login string `json:"login"` // Login name of the contributor in case it exists
+ AvatarLink string `json:"avatar_link"`
+ HomeLink string `json:"home_link"`
+ TotalCommits int64 `json:"total_commits"`
+ Weeks map[int64]*WeekData `json:"weeks"`
+}
+
+// ExtendedCommitStats contains information for commit stats with author data
+type ExtendedCommitStats struct {
+ Author *api.CommitUser `json:"author"`
+ Stats *api.CommitStats `json:"stats"`
+}
+
+const layout = time.DateOnly
+
+func findLastSundayBeforeDate(dateStr string) (string, error) {
+ date, err := time.Parse(layout, dateStr)
+ if err != nil {
+ return "", err
+ }
+
+ weekday := date.Weekday()
+ daysToSubtract := int(weekday) - int(time.Sunday)
+ if daysToSubtract < 0 {
+ daysToSubtract += 7
+ }
+
+ lastSunday := date.AddDate(0, 0, -daysToSubtract)
+ return lastSunday.Format(layout), nil
+}
+
+// GetContributorStats returns contributors stats for git commits for given revision or default branch
+func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) {
+ // as GetContributorStats is resource intensive we cache the result
+ cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision)
+ if !cache.IsExist(cacheKey) {
+ genReady := make(chan struct{})
+
+ // dont start multible async generations
+ _, run := generateLock.Load(cacheKey)
+ if run {
+ return nil, ErrAwaitGeneration
+ }
+
+ generateLock.Store(cacheKey, struct{}{})
+ // run generation async
+ go generateContributorStats(genReady, cache, cacheKey, repo, revision)
+
+ select {
+ case <-time.After(awaitGenerationTime):
+ return nil, ErrAwaitGeneration
+ case <-genReady:
+ // we got generation ready before timeout
+ break
+ }
+ }
+ // TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout)
+
+ switch v := cache.Get(cacheKey).(type) {
+ case error:
+ return nil, v
+ case map[string]*ContributorData:
+ return v, nil
+ default:
+ return nil, fmt.Errorf("unexpected type in cache detected")
+ }
+}
+
+// getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision
+func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) {
+ baseCommit, err := repo.GetCommit(revision)
+ if err != nil {
+ return nil, err
+ }
+ stdoutReader, stdoutWriter, err := os.Pipe()
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ _ = stdoutReader.Close()
+ _ = stdoutWriter.Close()
+ }()
+
+ gitCmd := git.NewCommand(repo.Ctx, "log", "--shortstat", "--no-merges", "--pretty=format:---%n%aN%n%aE%n%as", "--reverse")
+ // AddOptionFormat("--max-count=%d", limit)
+ gitCmd.AddDynamicArguments(baseCommit.ID.String())
+
+ var extendedCommitStats []*ExtendedCommitStats
+ stderr := new(strings.Builder)
+ err = gitCmd.Run(&git.RunOpts{
+ Dir: repo.Path,
+ Stdout: stdoutWriter,
+ Stderr: stderr,
+ PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+ _ = stdoutWriter.Close()
+ scanner := bufio.NewScanner(stdoutReader)
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line != "---" {
+ continue
+ }
+ scanner.Scan()
+ authorName := strings.TrimSpace(scanner.Text())
+ scanner.Scan()
+ authorEmail := strings.TrimSpace(scanner.Text())
+ scanner.Scan()
+ date := strings.TrimSpace(scanner.Text())
+ scanner.Scan()
+ stats := strings.TrimSpace(scanner.Text())
+ if authorName == "" || authorEmail == "" || date == "" || stats == "" {
+ // FIXME: find a better way to parse the output so that we will handle this properly
+ log.Warn("Something is wrong with git log output, skipping...")
+ log.Warn("authorName: %s, authorEmail: %s, date: %s, stats: %s", authorName, authorEmail, date, stats)
+ continue
+ }
+ // 1 file changed, 1 insertion(+), 1 deletion(-)
+ fields := strings.Split(stats, ",")
+
+ commitStats := api.CommitStats{}
+ for _, field := range fields[1:] {
+ parts := strings.Split(strings.TrimSpace(field), " ")
+ value, contributionType := parts[0], parts[1]
+ amount, _ := strconv.Atoi(value)
+
+ if strings.HasPrefix(contributionType, "insertion") {
+ commitStats.Additions = amount
+ } else {
+ commitStats.Deletions = amount
+ }
+ }
+ commitStats.Total = commitStats.Additions + commitStats.Deletions
+ scanner.Text() // empty line at the end
+
+ res := &ExtendedCommitStats{
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: authorName,
+ Email: authorEmail,
+ },
+ Date: date,
+ },
+ Stats: &commitStats,
+ }
+ extendedCommitStats = append(extendedCommitStats, res)
+
+ }
+ _ = stdoutReader.Close()
+ return nil
+ },
+ })
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get ContributorsCommitStats for repository.\nError: %w\nStderr: %s", err, stderr)
+ }
+
+ return extendedCommitStats, nil
+}
+
+func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey string, repo *repo_model.Repository, revision string) {
+ ctx := graceful.GetManager().HammerContext()
+
+ gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+ if err != nil {
+ err := fmt.Errorf("OpenRepository: %w", err)
+ _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+ return
+ }
+ defer closer.Close()
+
+ if len(revision) == 0 {
+ revision = repo.DefaultBranch
+ }
+ extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision)
+ if err != nil {
+ err := fmt.Errorf("ExtendedCommitStats: %w", err)
+ _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+ return
+ }
+ if len(extendedCommitStats) == 0 {
+ err := fmt.Errorf("no commit stats returned for revision '%s'", revision)
+ _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
+ return
+ }
+
+ layout := time.DateOnly
+
+ unknownUserAvatarLink := user_model.NewGhostUser().AvatarLinkWithSize(ctx, 0)
+ contributorsCommitStats := make(map[string]*ContributorData)
+ contributorsCommitStats["total"] = &ContributorData{
+ Name: "Total",
+ Weeks: make(map[int64]*WeekData),
+ }
+ total := contributorsCommitStats["total"]
+
+ for _, v := range extendedCommitStats {
+ userEmail := v.Author.Email
+ if len(userEmail) == 0 {
+ continue
+ }
+ u, _ := user_model.GetUserByEmail(ctx, userEmail)
+ if u != nil {
+ // update userEmail with user's primary email address so
+ // that different mail addresses will linked to same account
+ userEmail = u.GetEmail()
+ }
+ // duplicated logic
+ if _, ok := contributorsCommitStats[userEmail]; !ok {
+ if u == nil {
+ avatarLink := avatars.GenerateEmailAvatarFastLink(ctx, userEmail, 0)
+ if avatarLink == "" {
+ avatarLink = unknownUserAvatarLink
+ }
+ contributorsCommitStats[userEmail] = &ContributorData{
+ Name: v.Author.Name,
+ AvatarLink: avatarLink,
+ Weeks: make(map[int64]*WeekData),
+ }
+ } else {
+ contributorsCommitStats[userEmail] = &ContributorData{
+ Name: u.DisplayName(),
+ Login: u.LowerName,
+ AvatarLink: u.AvatarLinkWithSize(ctx, 0),
+ HomeLink: u.HomeLink(),
+ Weeks: make(map[int64]*WeekData),
+ }
+ }
+ }
+ // Update user statistics
+ user := contributorsCommitStats[userEmail]
+ startingOfWeek, _ := findLastSundayBeforeDate(v.Author.Date)
+
+ val, _ := time.Parse(layout, startingOfWeek)
+ week := val.UnixMilli()
+
+ if user.Weeks[week] == nil {
+ user.Weeks[week] = &WeekData{
+ Additions: 0,
+ Deletions: 0,
+ Commits: 0,
+ Week: week,
+ }
+ }
+ if total.Weeks[week] == nil {
+ total.Weeks[week] = &WeekData{
+ Additions: 0,
+ Deletions: 0,
+ Commits: 0,
+ Week: week,
+ }
+ }
+ user.Weeks[week].Additions += v.Stats.Additions
+ user.Weeks[week].Deletions += v.Stats.Deletions
+ user.Weeks[week].Commits++
+ user.TotalCommits++
+
+ // Update overall statistics
+ total.Weeks[week].Additions += v.Stats.Additions
+ total.Weeks[week].Deletions += v.Stats.Deletions
+ total.Weeks[week].Commits++
+ total.TotalCommits++
+ }
+
+ _ = cache.Put(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout)
+ generateLock.Delete(cacheKey)
+ if genDone != nil {
+ genDone <- struct{}{}
+ }
+}
diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go
new file mode 100644
index 0000000000..3801a5eee4
--- /dev/null
+++ b/services/repository/contributors_graph_test.go
@@ -0,0 +1,87 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "slices"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
+
+ "gitea.com/go-chi/cache"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepository_ContributorsGraph(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ assert.NoError(t, repo.LoadOwner(db.DefaultContext))
+ mockCache, err := cache.NewCacher(cache.Options{
+ Adapter: "memory",
+ Interval: 24 * 60,
+ })
+ assert.NoError(t, err)
+
+ generateContributorStats(nil, mockCache, "key", repo, "404ref")
+ err, isErr := mockCache.Get("key").(error)
+ assert.True(t, isErr)
+ assert.ErrorAs(t, err, &git.ErrNotExist{})
+
+ generateContributorStats(nil, mockCache, "key2", repo, "master")
+ data, isData := mockCache.Get("key2").(map[string]*ContributorData)
+ assert.True(t, isData)
+ var keys []string
+ for k := range data {
+ keys = append(keys, k)
+ }
+ slices.Sort(keys)
+ assert.EqualValues(t, []string{
+ "ethantkoenig@gmail.com",
+ "jimmy.praet@telenet.be",
+ "jon@allspice.io",
+ "total", // generated summary
+ }, keys)
+
+ assert.EqualValues(t, &ContributorData{
+ Name: "Ethan Koenig",
+ AvatarLink: "https://secure.gravatar.com/avatar/b42fb195faa8c61b8d88abfefe30e9e3?d=identicon",
+ TotalCommits: 1,
+ Weeks: map[int64]*WeekData{
+ 1511654400000: {
+ Week: 1511654400000, // sunday 2017-11-26
+ Additions: 3,
+ Deletions: 0,
+ Commits: 1,
+ },
+ },
+ }, data["ethantkoenig@gmail.com"])
+ assert.EqualValues(t, &ContributorData{
+ Name: "Total",
+ AvatarLink: "",
+ TotalCommits: 3,
+ Weeks: map[int64]*WeekData{
+ 1511654400000: {
+ Week: 1511654400000, // sunday 2017-11-26 (2017-11-26 20:31:18 -0800)
+ Additions: 3,
+ Deletions: 0,
+ Commits: 1,
+ },
+ 1607817600000: {
+ Week: 1607817600000, // sunday 2020-12-13 (2020-12-15 15:23:11 -0500)
+ Additions: 10,
+ Deletions: 0,
+ Commits: 1,
+ },
+ 1624752000000: {
+ Week: 1624752000000, // sunday 2021-06-27 (2021-06-29 21:54:09 +0200)
+ Additions: 2,
+ Deletions: 0,
+ Commits: 1,
+ },
+ },
+ }, data["total"])
+}
diff --git a/services/repository/create.go b/services/repository/create.go
index d5942e6c07..c3743cacba 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -158,7 +158,7 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re
}
// Apply changes and commit.
- if err = repo_module.InitRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
+ if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil {
return fmt.Errorf("initRepoCommit: %w", err)
}
}
@@ -174,15 +174,11 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re
}
repo.DefaultBranch = setting.Repository.DefaultBranch
+ repo.DefaultWikiBranch = setting.Repository.DefaultBranch
if len(opts.DefaultBranch) > 0 {
repo.DefaultBranch = opts.DefaultBranch
- gitRepo, err := gitrepo.OpenRepository(ctx, repo)
- if err != nil {
- return fmt.Errorf("openRepository: %w", err)
- }
- defer gitRepo.Close()
- if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err)
}
@@ -241,6 +237,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
TrustModel: opts.TrustModel,
IsMirror: opts.IsMirror,
DefaultBranch: opts.DefaultBranch,
+ DefaultWikiBranch: setting.Repository.DefaultBranch,
ObjectFormatName: opts.ObjectFormatName,
}
diff --git a/services/repository/create_test.go b/services/repository/create_test.go
index b3e1f0550c..41e6b615db 100644
--- a/services/repository/create_test.go
+++ b/services/repository/create_test.go
@@ -21,12 +21,12 @@ import (
func TestIncludesAllRepositoriesTeams(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testTeamRepositories := func(teamID int64, repoIds []int64) {
+ 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 {
+ assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name)
+ for i, rid := range repoIDs {
if rid > 0 {
assert.True(t, HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i)
}
@@ -52,12 +52,12 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
// Create repos.
- repoIds := make([]int64, 0)
+ repoIDs := make([]int64, 0)
for i := 0; i < 3; i++ {
r, err := CreateRepositoryDirectly(db.DefaultContext, 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)
+ repoIDs = append(repoIDs, r.ID)
}
}
// Get fresh copy of Owner team after creating repos.
@@ -93,10 +93,10 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
},
}
teamRepos := [][]int64{
- repoIds,
- repoIds,
+ repoIDs,
+ repoIDs,
{},
- repoIds,
+ repoIDs,
{},
}
for i, team := range teams {
@@ -109,7 +109,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
// Update teams and check repositories.
teams[3].IncludesAllRepositories = false
teams[4].IncludesAllRepositories = true
- teamRepos[4] = repoIds
+ teamRepos[4] = repoIDs
for i, team := range teams {
assert.NoError(t, models.UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name)
testTeamRepositories(team.ID, teamRepos[i])
@@ -119,27 +119,27 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: "repo-last"})
assert.NoError(t, err, "CreateRepository last")
if r != nil {
- repoIds = append(repoIds, r.ID)
+ repoIDs = append(repoIDs, r.ID)
}
- teamRepos[0] = repoIds
- teamRepos[1] = repoIds
- teamRepos[4] = repoIds
+ 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, DeleteRepositoryDirectly(db.DefaultContext, user, repoIds[0]), "DeleteRepository")
- teamRepos[0] = repoIds[1:]
- teamRepos[1] = repoIds[1:]
- teamRepos[3] = repoIds[1:3]
- teamRepos[4] = repoIds[1:]
+ assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, 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 {
+ for i, rid := range repoIDs {
if i > 0 { // first repo already deleted.
assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i)
}
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 426b4dbead..803090cccf 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -28,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
"xorm.io/builder"
)
@@ -163,6 +164,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
&actions_model.ActionScheduleSpec{RepoID: repoID},
&actions_model.ActionSchedule{RepoID: repoID},
&actions_model.ActionArtifact{RepoID: repoID},
+ &actions_model.ActionRunnerToken{RepoID: repoID},
); err != nil {
return fmt.Errorf("deleteBeans: %w", err)
}
@@ -278,7 +280,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
committer.Close()
if needRewriteKeysFile {
- if err := asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+ if err := asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
log.Error("RewriteAllPublicKeys failed: %v", err)
}
}
@@ -365,24 +367,26 @@ func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *r
}
}
- teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID)
+ teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
+ TeamID: t.ID,
+ })
if err != nil {
- return fmt.Errorf("getTeamUsersByTeamID: %w", err)
+ return fmt.Errorf("GetTeamMembers: %w", err)
}
- for _, teamUser := range teamUsers {
- has, err := access_model.HasAccess(ctx, teamUser.UID, repo)
+ for _, member := range teamMembers {
+ has, err := access_model.HasAccess(ctx, member.ID, repo)
if err != nil {
return err
} else if has {
continue
}
- if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil {
+ if err = repo_model.WatchRepo(ctx, member, repo, 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 {
+ if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
return err
}
}
diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go
index 613b46d8f6..451a182155 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -28,7 +28,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
- log.Error("%v", err)
+ log.Error("NewTemporaryUploadRepository failed: %v", err)
}
defer t.Close()
if err := t.Clone(opts.OldBranch, false); err != nil {
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index 937db472e6..e0dad29273 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -5,59 +5,20 @@ package files
import (
"context"
- "fmt"
asymkey_model "code.gitea.io/gitea/models/asymkey"
- git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/automerge"
)
-// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
-// NOTE: All text-values will be trimmed from whitespaces.
-// Requires: Repo, Creator, SHA
-func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
- // confirm that commit is exist
- gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+// CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch
+func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, branch string) (*git.DivergeObject, error) {
+ divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch)
if err != nil {
- return fmt.Errorf("OpenRepository[%s]: %w", gitrepo.RepoGitURL(repo), err)
+ return nil, err
}
- defer closer.Close()
-
- objectFormat, err := gitRepo.GetObjectFormat()
- if err != nil {
- return fmt.Errorf("GetObjectFormat[%s]: %w", gitrepo.RepoGitURL(repo), err)
- }
- commit, err := gitRepo.GetCommit(sha)
- if err != nil {
- gitRepo.Close()
- return fmt.Errorf("GetCommit[%s]: %w", sha, err)
- } else if len(sha) != objectFormat.FullLength() {
- // use complete commit sha
- sha = commit.ID.String()
- }
- gitRepo.Close()
-
- if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
- Repo: repo,
- Creator: creator,
- SHA: commit.ID,
- CommitStatus: status,
- }); err != nil {
- return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
- }
-
- if status.State.IsSuccess() {
- if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
- return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
- }
- }
-
- return nil
+ return &divergence, nil
}
// GetPayloadCommitVerification returns the verification information of a commit
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index c278d7f835..95e7c7087c 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -220,7 +220,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
}
}
// Handle links
- if entry.IsRegular() || entry.IsLink() {
+ if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
if err != nil {
return nil, err
@@ -270,3 +270,28 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
Content: content,
}, nil
}
+
+// TryGetContentLanguage tries to get the (linguist) language of the file content
+func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (string, error) {
+ indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitID)
+ if err != nil {
+ return "", err
+ }
+
+ defer deleteTemporaryFile()
+
+ filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
+ CachedOnly: true,
+ Attributes: []string{git.AttributeLinguistLanguage, git.AttributeGitlabLanguage},
+ Filenames: []string{treePath},
+ IndexFile: indexFilename,
+ WorkTree: worktree,
+ })
+ if err != nil {
+ return "", err
+ }
+
+ language := git.TryReadLanguageAttribute(filename2attribute2info[treePath])
+
+ return language.Value(), nil
+}
diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go
index d50847789a..4811f9d327 100644
--- a/services/repository/files/content_test.go
+++ b/services/repository/files/content_test.go
@@ -7,9 +7,9 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/contexttest"
_ "code.gitea.io/gitea/models/actions"
diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go
index 91c878e505..63aff9b0e3 100644
--- a/services/repository/files/diff_test.go
+++ b/services/repository/files/diff_test.go
@@ -8,8 +8,8 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/gitdiff"
"github.com/stretchr/testify/assert"
diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go
index 675ddbddb3..a5b3aad91e 100644
--- a/services/repository/files/file_test.go
+++ b/services/repository/files/file_test.go
@@ -7,10 +7,10 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index f6d5643dc9..e5f7e2af96 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -111,7 +111,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
- log.Error("%v", err)
+ log.Error("NewTemporaryUploadRepository failed: %v", err)
}
defer t.Close()
if err := t.Clone(opts.OldBranch, true); err != nil {
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index 9d3185c3fc..e3a7f3b8b0 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -37,7 +37,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
}
apiURL := repo.APIURL()
apiURLLen := len(apiURL)
- objectFormat, _ := gitRepo.GetObjectFormat()
+ objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
hashLen := objectFormat.FullLength()
const gitBlobsPath = "/git/blobs/"
diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go
index 528ef500df..508f20090d 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -7,8 +7,8 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/contexttest"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index f223daf3a9..f029a9aefe 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -40,7 +40,7 @@ type ChangeRepoFile struct {
Operation string
TreePath string
FromTreePath string
- ContentReader io.Reader
+ ContentReader io.ReadSeeker
SHA string
Options *RepoFileOptions
}
@@ -143,7 +143,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
- log.Error("%v", err)
+ log.Error("NewTemporaryUploadRepository failed: %v", err)
}
defer t.Close()
hasOldBranch := true
@@ -448,6 +448,10 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file
return err
}
if !exist {
+ _, err := file.ContentReader.Seek(0, io.SeekStart)
+ if err != nil {
+ return err
+ }
if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil {
if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil {
return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
diff --git a/services/repository/fork.go b/services/repository/fork.go
index 1c532dd70e..6b1e16b1c3 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -52,6 +52,14 @@ type ForkRepoOptions struct {
// ForkRepository forks a repository
func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
+ if err := opts.BaseRepo.LoadOwner(ctx); err != nil {
+ return nil, err
+ }
+
+ if user_model.IsUserBlockedBy(ctx, doer, opts.BaseRepo.Owner.ID) {
+ return nil, user_model.ErrBlockedUser
+ }
+
// Fork is prohibited, if user has reached maximum limit of repositories
if !owner.CanForkRepo() {
return nil, repo_model.ErrReachLimitOfRepo{
diff --git a/modules/repository/generate.go b/services/repository/generate.go
similarity index 93%
rename from modules/repository/generate.go
rename to services/repository/generate.go
index d7bb5bab30..cf05ab28a7 100644
--- a/modules/repository/generate.go
+++ b/services/repository/generate.go
@@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
@@ -94,7 +95,7 @@ type GiteaTemplate struct {
}
// Globs parses the .gitea/template globs or returns them if they were already parsed
-func (gt GiteaTemplate) Globs() []glob.Glob {
+func (gt *GiteaTemplate) Globs() []glob.Glob {
if gt.globs != nil {
return gt.globs
}
@@ -242,7 +243,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
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) {
@@ -271,12 +272,7 @@ func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *r
repo.DefaultBranch = templateRepo.DefaultBranch
}
- gitRepo, err := gitrepo.OpenRepository(ctx, repo)
- if err != nil {
- return fmt.Errorf("openRepository: %w", err)
- }
- defer gitRepo.Close()
- if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return fmt.Errorf("setDefaultBranch: %w", err)
}
if err = UpdateRepository(ctx, repo, false); err != nil {
@@ -292,7 +288,7 @@ func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_mo
return err
}
- if err := UpdateRepoSize(ctx, generateRepo); err != nil {
+ if err := repo_module.UpdateRepoSize(ctx, generateRepo); err != nil {
return fmt.Errorf("failed to update size for repository: %w", err)
}
@@ -323,8 +319,8 @@ func (gro GenerateRepoOptions) IsValid() bool {
gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
}
-// GenerateRepository generates a repository from a template
-func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
+// generateRepository generates a repository from a template
+func generateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
generateRepo := &repo_model.Repository{
OwnerID: owner.ID,
Owner: owner,
@@ -341,7 +337,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
ObjectFormatName: templateRepo.ObjectFormatName,
}
- if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
+ if err = repo_module.CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
return nil, err
}
@@ -358,11 +354,11 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
}
}
- if err = CheckInitRepository(ctx, generateRepo, generateRepo.ObjectFormatName); err != nil {
+ if err = repo_module.CheckInitRepository(ctx, generateRepo, generateRepo.ObjectFormatName); err != nil {
return generateRepo, err
}
- if err = CheckDaemonExportOK(ctx, generateRepo); err != nil {
+ if err = repo_module.CheckDaemonExportOK(ctx, generateRepo); err != nil {
return generateRepo, fmt.Errorf("checkDaemonExportOK: %w", err)
}
diff --git a/modules/repository/generate_test.go b/services/repository/generate_test.go
similarity index 100%
rename from modules/repository/generate_test.go
rename to services/repository/generate_test.go
diff --git a/services/repository/init.go b/services/repository/init.go
new file mode 100644
index 0000000000..817fa4abd7
--- /dev/null
+++ b/services/repository/init.go
@@ -0,0 +1,83 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
+)
+
+// 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)
+
+ sig := u.NewGitSig()
+ // Because this may call hooks we should pass in the environment
+ env := append(os.Environ(),
+ "GIT_AUTHOR_NAME="+sig.Name,
+ "GIT_AUTHOR_EMAIL="+sig.Email,
+ "GIT_AUTHOR_DATE="+commitTimeStr,
+ "GIT_COMMITTER_DATE="+commitTimeStr,
+ )
+ committerName := sig.Name
+ committerEmail := sig.Email
+
+ if stdout, _, err := git.NewCommand(ctx, "add", "--all").
+ SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)).
+ RunStdString(&git.RunOpts{Dir: tmpPath}); err != nil {
+ log.Error("git add --all failed: Stdout: %s\nError: %v", stdout, err)
+ return fmt.Errorf("git add --all: %w", err)
+ }
+
+ cmd := git.NewCommand(ctx, "commit", "--message=Initial commit").
+ AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
+
+ sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
+ if sign {
+ cmd.AddOptionFormat("-S%s", keyID)
+
+ if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
+ // need to set the committer to the KeyID owner
+ committerName = signer.Name
+ committerEmail = signer.Email
+ }
+ } else {
+ cmd.AddArguments("--no-gpg-sign")
+ }
+
+ env = append(env,
+ "GIT_COMMITTER_NAME="+committerName,
+ "GIT_COMMITTER_EMAIL="+committerEmail,
+ )
+
+ if stdout, _, err := cmd.
+ SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
+ RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil {
+ log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err)
+ return fmt.Errorf("git commit: %w", err)
+ }
+
+ if len(defaultBranch) == 0 {
+ defaultBranch = setting.Repository.DefaultBranch
+ }
+
+ if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch).
+ SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)).
+ RunStdString(&git.RunOpts{Dir: tmpPath, Env: repo_module.InternalPushingEnvironment(u, repo)}); err != nil {
+ log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err)
+ return fmt.Errorf("git push: %w", err)
+ }
+
+ return nil
+}
diff --git a/services/repository/lfs.go b/services/repository/lfs.go
index 4504f796bd..4d48881b87 100644
--- a/services/repository/lfs.go
+++ b/services/repository/lfs.go
@@ -79,7 +79,7 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R
store := lfs.NewContentStore()
errStop := errors.New("STOPERR")
- objectFormat, _ := gitRepo.GetObjectFormat()
+ objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
err = git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, metaObject *git_model.LFSMetaObject, count int64) error {
if opts.NumberToCheckPerRepo > 0 && total > opts.NumberToCheckPerRepo {
diff --git a/services/repository/migrate.go b/services/repository/migrate.go
new file mode 100644
index 0000000000..cbfc14a39c
--- /dev/null
+++ b/services/repository/migrate.go
@@ -0,0 +1,286 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/migration"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+)
+
+func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) {
+ wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
+ wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
+ if wikiRemotePath == "" {
+ return "", nil
+ }
+
+ if err := util.RemoveAll(wikiPath); err != nil {
+ return "", fmt.Errorf("failed to remove existing wiki dir %q, err: %w", wikiPath, err)
+ }
+
+ cleanIncompleteWikiPath := func() {
+ if err := util.RemoveAll(wikiPath); err != nil {
+ log.Error("Failed to remove incomplete wiki dir %q, err: %v", wikiPath, err)
+ }
+ }
+ if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
+ Mirror: true,
+ Quiet: true,
+ Timeout: migrateTimeout,
+ SkipTLSVerify: setting.Migrations.SkipTLSVerify,
+ }); err != nil {
+ log.Error("Clone wiki failed, err: %v", err)
+ cleanIncompleteWikiPath()
+ return "", err
+ }
+
+ if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
+ cleanIncompleteWikiPath()
+ return "", err
+ }
+
+ defaultBranch, err := git.GetDefaultBranch(ctx, wikiPath)
+ if err != nil {
+ cleanIncompleteWikiPath()
+ return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err)
+ }
+
+ return defaultBranch, nil
+}
+
+// MigrateRepositoryGitData starts migrating git related data after created migrating repository
+func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
+ repo *repo_model.Repository, opts migration.MigrateOptions,
+ httpTransport *http.Transport,
+) (*repo_model.Repository, error) {
+ repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
+
+ if u.IsOrganization() {
+ t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
+ if err != nil {
+ return nil, err
+ }
+ repo.NumWatches = t.NumMembers
+ } else {
+ repo.NumWatches = 1
+ }
+
+ migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
+
+ if err := util.RemoveAll(repoPath); err != nil {
+ return repo, fmt.Errorf("failed to remove existing repo dir %q, err: %w", repoPath, err)
+ }
+
+ if err := git.Clone(ctx, opts.CloneAddr, repoPath, git.CloneRepoOptions{
+ Mirror: true,
+ Quiet: true,
+ Timeout: migrateTimeout,
+ SkipTLSVerify: setting.Migrations.SkipTLSVerify,
+ }); err != nil {
+ if errors.Is(err, context.DeadlineExceeded) {
+ return repo, fmt.Errorf("clone timed out, consider increasing [git.timeout] MIGRATE in app.ini, underlying err: %w", err)
+ }
+ return repo, fmt.Errorf("clone error: %w", err)
+ }
+
+ if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
+ return repo, err
+ }
+
+ if opts.Wiki {
+ defaultWikiBranch, err := cloneWiki(ctx, u, opts, migrateTimeout)
+ if err != nil {
+ return repo, fmt.Errorf("clone wiki error: %w", err)
+ }
+ repo.DefaultWikiBranch = defaultWikiBranch
+ }
+
+ if repo.OwnerID == u.ID {
+ repo.Owner = u
+ }
+
+ if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
+ return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
+ }
+
+ if stdout, _, err := git.NewCommand(ctx, "update-server-info").
+ SetDescription(fmt.Sprintf("MigrateRepositoryGitData(git update-server-info): %s", repoPath)).
+ RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
+ log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+ return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
+ }
+
+ gitRepo, err := git.OpenRepository(ctx, repoPath)
+ if err != nil {
+ return repo, fmt.Errorf("OpenRepository: %w", err)
+ }
+ defer gitRepo.Close()
+
+ repo.IsEmpty, err = gitRepo.IsEmpty()
+ if err != nil {
+ return repo, fmt.Errorf("git.IsEmpty: %w", err)
+ }
+
+ if !repo.IsEmpty {
+ if len(repo.DefaultBranch) == 0 {
+ // Try to get HEAD branch and set it as default branch.
+ headBranch, err := gitRepo.GetHEADBranch()
+ if err != nil {
+ return repo, fmt.Errorf("GetHEADBranch: %w", err)
+ }
+ if headBranch != nil {
+ repo.DefaultBranch = headBranch.Name
+ }
+ }
+
+ if _, err := repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, u.ID); err != nil {
+ return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err)
+ }
+
+ if !opts.Releases {
+ // note: this will greatly improve release (tag) sync
+ // for pull-mirrors with many tags
+ repo.IsMirror = opts.Mirror
+ if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
+ log.Error("Failed to synchronize tags to releases for repository: %v", err)
+ }
+ }
+
+ if opts.LFS {
+ endpoint := lfs.DetermineEndpoint(opts.CloneAddr, opts.LFSEndpoint)
+ lfsClient := lfs.NewClient(endpoint, httpTransport)
+ if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, repo, gitRepo, lfsClient); err != nil {
+ log.Error("Failed to store missing LFS objects for repository: %v", err)
+ }
+ }
+ }
+
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer committer.Close()
+
+ if opts.Mirror {
+ remoteAddress, err := util.SanitizeURL(opts.CloneAddr)
+ if err != nil {
+ return repo, err
+ }
+ mirrorModel := repo_model.Mirror{
+ RepoID: repo.ID,
+ Interval: setting.Mirror.DefaultInterval,
+ EnablePrune: true,
+ NextUpdateUnix: timeutil.TimeStampNow().AddDuration(setting.Mirror.DefaultInterval),
+ LFS: opts.LFS,
+ RemoteAddress: remoteAddress,
+ }
+ if opts.LFS {
+ mirrorModel.LFSEndpoint = opts.LFSEndpoint
+ }
+
+ if opts.MirrorInterval != "" {
+ parsedInterval, err := time.ParseDuration(opts.MirrorInterval)
+ if err != nil {
+ log.Error("Failed to set Interval: %v", err)
+ return repo, err
+ }
+ if parsedInterval == 0 {
+ mirrorModel.Interval = 0
+ mirrorModel.NextUpdateUnix = 0
+ } else if parsedInterval < setting.Mirror.MinInterval {
+ err := fmt.Errorf("interval %s is set below Minimum Interval of %s", parsedInterval, setting.Mirror.MinInterval)
+ log.Error("Interval: %s is too frequent", opts.MirrorInterval)
+ return repo, err
+ } else {
+ mirrorModel.Interval = parsedInterval
+ mirrorModel.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(parsedInterval)
+ }
+ }
+
+ if err = repo_model.InsertMirror(ctx, &mirrorModel); err != nil {
+ return repo, fmt.Errorf("InsertOne: %w", err)
+ }
+
+ repo.IsMirror = true
+ if err = UpdateRepository(ctx, repo, false); err != nil {
+ return nil, err
+ }
+
+ // this is necessary for sync local tags from remote
+ configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName())
+ if stdout, _, err := git.NewCommand(ctx, "config").
+ AddOptionValues("--add", configName, `+refs/tags/*:refs/tags/*`).
+ RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
+ log.Error("MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err)
+ return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add +refs/tags/*:refs/tags/*): %w", err)
+ }
+ } else {
+ if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
+ log.Error("Failed to update size for repository: %v", err)
+ }
+ if repo, err = CleanUpMigrateInfo(ctx, repo); err != nil {
+ return nil, err
+ }
+ }
+
+ return repo, committer.Commit()
+}
+
+// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
+// This also removes possible user credentials.
+func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
+ cmd := git.NewCommand(ctx, "remote", "rm", "origin")
+ // if the origin does not exist
+ _, stderr, err := cmd.RunStdString(&git.RunOpts{
+ Dir: repoPath,
+ })
+ if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
+ return err
+ }
+ return nil
+}
+
+// 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) {
+ repoPath := repo.RepoPath()
+ if err := gitrepo.CreateDelegateHooks(ctx, repo, false); err != nil {
+ return repo, fmt.Errorf("createDelegateHooks: %w", err)
+ }
+ if repo.HasWiki() {
+ if err := gitrepo.CreateDelegateHooks(ctx, repo, true); err != nil {
+ return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
+ }
+ }
+
+ _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+ return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
+ }
+
+ if repo.HasWiki() {
+ if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil {
+ return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
+ }
+ }
+
+ return repo, UpdateRepository(ctx, repo, false)
+}
diff --git a/services/repository/push.go b/services/repository/push.go
index bedcf6f252..39843249a5 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -11,7 +11,6 @@ import (
"time"
"code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
@@ -93,11 +92,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
defer gitRepo.Close()
- objectFormat, err := gitRepo.GetObjectFormat()
- if err != nil {
- return fmt.Errorf("unknown repository ObjectFormat [%s]: %w", repo.FullName(), err)
- }
-
if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
return fmt.Errorf("Failed to update size for repository: %v", err)
}
@@ -105,6 +99,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
addTags := make([]string, 0, len(optsList))
delTags := make([]string, 0, len(optsList))
var pusher *user_model.User
+ objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
for _, opts := range optsList {
log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName)
@@ -187,7 +182,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
repo.DefaultBranch = refName
repo.IsEmpty = false
if repo.DefaultBranch != setting.Repository.DefaultBranch {
- if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
+ if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
if !git.IsErrUnsupportedVersion(err) {
return err
}
@@ -225,6 +220,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
}
+ // delete cache for divergence
+ if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
+ log.Error("DelDivergenceFromCache: %v", err)
+ }
+
commits := repo_module.GitToPushCommits(l)
commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
@@ -263,10 +263,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
}
- if err = syncBranchToDB(ctx, repo.ID, opts.PusherID, branch, newCommit); err != nil {
- return fmt.Errorf("git_model.UpdateBranch %s:%s failed: %v", repo.FullName(), branch, err)
- }
-
notify_service.PushCommits(ctx, pusher, repo, opts, commits)
// Cache for big repository
@@ -279,10 +275,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
// close all related pulls
log.Error("close related pull request failed: %v", err)
}
-
- if err := git_model.AddDeletedBranch(ctx, repo.ID, branch, pusher.ID); err != nil {
- return fmt.Errorf("AddDeletedBranch %s:%s failed: %v", repo.FullName(), branch, err)
- }
}
// Even if user delete a branch on a repository which he didn't watch, he will be watch that.
@@ -321,14 +313,9 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
return nil
}
- lowerTags := make([]string, 0, len(tags))
- for _, tag := range tags {
- lowerTags = append(lowerTags, strings.ToLower(tag))
- }
-
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
- TagNames: lowerTags,
+ TagNames: tags,
})
if err != nil {
return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
@@ -338,6 +325,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
relMap[rel.LowerTagName] = rel
}
+ lowerTags := make([]string, 0, len(tags))
+ for _, tag := range tags {
+ lowerTags = append(lowerTags, strings.ToLower(tag))
+ }
+
newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap))
emailToUser := make(map[string]*user_model.User)
diff --git a/services/repository/template.go b/services/repository/template.go
index 06cf05026f..36a680c8e2 100644
--- a/services/repository/template.go
+++ b/services/repository/template.go
@@ -11,7 +11,6 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- repo_module "code.gitea.io/gitea/modules/repository"
notify_service "code.gitea.io/gitea/services/notify"
)
@@ -63,7 +62,7 @@ func GenerateProtectedBranch(ctx context.Context, templateRepo, generateRepo *re
}
// GenerateRepository generates a repository from a template
-func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts repo_module.GenerateRepoOptions) (_ *repo_model.Repository, err error) {
+func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
if !doer.IsAdmin && !owner.CanCreateRepo() {
return nil, repo_model.ErrReachLimitOfRepo{
Limit: owner.MaxRepoCreation,
@@ -72,14 +71,14 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
var generateRepo *repo_model.Repository
if err = db.WithTx(ctx, func(ctx context.Context) error {
- generateRepo, err = repo_module.GenerateRepository(ctx, doer, owner, templateRepo, opts)
+ generateRepo, err = generateRepository(ctx, doer, owner, templateRepo, opts)
if err != nil {
return err
}
// Git Content
if opts.GitContent && !templateRepo.IsEmpty {
- if err = repo_module.GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
+ if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
return err
}
}
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index b6d030b850..c929ad03fe 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -140,9 +140,9 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
}
// Remove redundant collaborators.
- collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{})
+ collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repo.ID})
if err != nil {
- return fmt.Errorf("getCollaborators: %w", err)
+ return fmt.Errorf("GetCollaborators: %w", err)
}
// Dummy object.
@@ -202,13 +202,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
return fmt.Errorf("decrease old owner repository count: %w", err)
}
- if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil {
+ if err := repo_model.WatchRepo(ctx, doer, repo, true); err != nil {
return fmt.Errorf("watchRepo: %w", err)
}
// Remove watch for organization.
if oldOwner.IsOrganization() {
- if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil {
+ if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil {
return fmt.Errorf("watchRepo [false]: %w", err)
}
}
@@ -365,6 +365,10 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
return TransferOwnership(ctx, doer, newOwner, repo, teams)
}
+ if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) {
+ return user_model.ErrBlockedUser
+ }
+
// If new owner is an org and user can create repos he can transfer directly too
if newOwner.IsOrganization() {
allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
diff --git a/services/user/block.go b/services/user/block.go
new file mode 100644
index 0000000000..0b3b618aae
--- /dev/null
+++ b/services/user/block.go
@@ -0,0 +1,308 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ org_model "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+ if blocker.ID == blockee.ID {
+ return false
+ }
+ if doer.ID == blockee.ID {
+ return false
+ }
+
+ if blockee.IsOrganization() {
+ return false
+ }
+
+ if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+ return false
+ }
+
+ if blocker.IsOrganization() {
+ org := org_model.OrgFromUser(blocker)
+ if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
+ return false
+ }
+ if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+ return false
+ }
+ } else if !doer.IsAdmin && doer.ID != blocker.ID {
+ return false
+ }
+
+ return true
+}
+
+func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
+ if doer.ID == blockee.ID {
+ return false
+ }
+
+ if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
+ return false
+ }
+
+ if blocker.IsOrganization() {
+ org := org_model.OrgFromUser(blocker)
+ if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
+ return false
+ }
+ } else if !doer.IsAdmin && doer.ID != blocker.ID {
+ return false
+ }
+
+ return true
+}
+
+func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
+ if blockee.IsOrganization() {
+ return user_model.ErrBlockOrganization
+ }
+
+ if !CanBlockUser(ctx, doer, blocker, blockee) {
+ return user_model.ErrCanNotBlock
+ }
+
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ // unfollow each other
+ if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
+ return err
+ }
+ if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
+ return err
+ }
+
+ // unstar each other
+ if err := unstarRepos(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := unstarRepos(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // unwatch each others repositories
+ if err := unwatchRepos(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := unwatchRepos(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // unassign each other from issues
+ if err := unassignIssues(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := unassignIssues(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // remove each other from repository collaborations
+ if err := removeCollaborations(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := removeCollaborations(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ // cancel each other repository transfers
+ if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
+ return err
+ }
+ if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
+ return err
+ }
+
+ return db.Insert(ctx, &user_model.Blocking{
+ BlockerID: blocker.ID,
+ BlockeeID: blockee.ID,
+ Note: note,
+ })
+ })
+}
+
+func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
+ opts := &repo_model.StarredReposOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ StarrerID: starrer.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ repos, err := repo_model.GetStarredRepos(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(repos) == 0 {
+ return nil
+ }
+
+ for _, repo := range repos {
+ if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
+ opts := &repo_model.WatchedReposOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ WatcherID: watcher.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(repos) == 0 {
+ return nil
+ }
+
+ for _, repo := range repos {
+ if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
+ transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
+ SenderID: sender.ID,
+ RecipientID: recipient.ID,
+ })
+ if err != nil {
+ return err
+ }
+
+ for _, transfer := range transfers {
+ repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
+ if err != nil {
+ return err
+ }
+
+ if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
+ opts := &issues_model.AssignedIssuesOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ AssigneeID: assignee.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(issues) == 0 {
+ return nil
+ }
+
+ for _, issue := range issues {
+ if err := issue.LoadAssignees(ctx); err != nil {
+ return err
+ }
+
+ if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
+ opts := &repo_model.FindCollaborationOptions{
+ ListOptions: db.ListOptions{
+ Page: 1,
+ PageSize: 25,
+ },
+ CollaboratorID: collaborator.ID,
+ RepoOwnerID: repoOwner.ID,
+ }
+
+ for {
+ collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
+ if err != nil {
+ return err
+ }
+
+ if len(collaborations) == 0 {
+ return nil
+ }
+
+ for _, collaboration := range collaborations {
+ repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
+ if err != nil {
+ return err
+ }
+
+ if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
+ return err
+ }
+ }
+
+ opts.Page++
+ }
+}
+
+func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
+ if blockee.IsOrganization() {
+ return user_model.ErrBlockOrganization
+ }
+
+ if !CanUnblockUser(ctx, doer, blocker, blockee) {
+ return user_model.ErrCanNotUnblock
+ }
+
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
+ if err != nil {
+ return err
+ }
+ if block != nil {
+ _, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
+ return err
+ }
+ return nil
+ })
+}
diff --git a/services/user/block_test.go b/services/user/block_test.go
new file mode 100644
index 0000000000..aec3e03cf3
--- /dev/null
+++ b/services/user/block_test.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCanBlockUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+ org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+
+ // Doer can't self block
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user1))
+ // Blocker can't be blockee
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user2))
+ // Can't block already blocked user
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, user29))
+ // Blockee can't be an organization
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, user2, org3))
+ // Doer must be blocker or admin
+ assert.False(t, CanBlockUser(db.DefaultContext, user2, user4, user29))
+ // Organization can't block a member
+ assert.False(t, CanBlockUser(db.DefaultContext, user1, org3, user4))
+ // Doer must be organization owner or admin if blocker is an organization
+ assert.False(t, CanBlockUser(db.DefaultContext, user4, org3, user2))
+
+ assert.True(t, CanBlockUser(db.DefaultContext, user1, user2, user4))
+ assert.True(t, CanBlockUser(db.DefaultContext, user2, user2, user4))
+ assert.True(t, CanBlockUser(db.DefaultContext, user2, org3, user29))
+}
+
+func TestCanUnblockUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user28 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28})
+ user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+ org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
+
+ // Doer can't self unblock
+ assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user1))
+ // Can't unblock not blocked user
+ assert.False(t, CanUnblockUser(db.DefaultContext, user1, user2, user28))
+ // Doer must be blocker or admin
+ assert.False(t, CanUnblockUser(db.DefaultContext, user28, user2, user29))
+ // Doer must be organization owner or admin if blocker is an organization
+ assert.False(t, CanUnblockUser(db.DefaultContext, user2, org17, user28))
+
+ assert.True(t, CanUnblockUser(db.DefaultContext, user1, user2, user29))
+ assert.True(t, CanUnblockUser(db.DefaultContext, user2, user2, user29))
+ assert.True(t, CanUnblockUser(db.DefaultContext, user1, org17, user28))
+}
diff --git a/services/user/delete.go b/services/user/delete.go
index 000910319a..889da3eb67 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -92,6 +92,9 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
&pull_model.ReviewState{UserID: u.ID},
&user_model.Redirect{RedirectUserID: u.ID},
&actions_model.ActionRunner{OwnerID: u.ID},
+ &user_model.Blocking{BlockerID: u.ID},
+ &user_model.Blocking{BlockeeID: u.ID},
+ &actions_model.ActionRunnerToken{OwnerID: u.ID},
); err != nil {
return fmt.Errorf("deleteBeans: %w", err)
}
diff --git a/services/user/email.go b/services/user/email.go
index 07e19bc688..5c0de708e9 100644
--- a/services/user/email.go
+++ b/services/user/email.go
@@ -14,12 +14,13 @@ import (
"code.gitea.io/gitea/modules/util"
)
-func AddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
+// AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address
+func AdminAddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
if strings.EqualFold(u.Email, emailStr) {
return nil
}
- if err := user_model.ValidateEmail(emailStr); err != nil {
+ if err := user_model.ValidateEmailForAdmin(emailStr); err != nil {
return err
}
diff --git a/services/user/email_test.go b/services/user/email_test.go
index 8f419b69f9..b40f86b6a6 100644
--- a/services/user/email_test.go
+++ b/services/user/email_test.go
@@ -10,11 +10,13 @@ import (
organization_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "github.com/gobwas/glob"
"github.com/stretchr/testify/assert"
)
-func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
+func TestAdminAddOrSetPrimaryEmailAddress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 27})
@@ -28,7 +30,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
assert.NotEqual(t, "new-primary@example.com", primary.Email)
assert.Equal(t, user.Email, primary.Email)
- assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com"))
+ assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary@example.com"))
primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
assert.NoError(t, err)
@@ -39,7 +41,19 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, emails, 2)
- assert.NoError(t, AddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com"))
+ setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")}
+ defer func() {
+ setting.Service.EmailDomainAllowList = []glob.Glob{}
+ }()
+
+ assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "new-primary2@example2.com"))
+
+ primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
+ assert.NoError(t, err)
+ assert.Equal(t, "new-primary2@example2.com", primary.Email)
+ assert.Equal(t, user.Email, primary.Email)
+
+ assert.NoError(t, AdminAddOrSetPrimaryEmailAddress(db.DefaultContext, user, "user27@example.com"))
primary, err = user_model.GetPrimaryEmailAddressOfUser(db.DefaultContext, user.ID)
assert.NoError(t, err)
@@ -48,7 +62,7 @@ func TestAddOrSetPrimaryEmailAddress(t *testing.T) {
emails, err = user_model.GetEmailAddresses(db.DefaultContext, user.ID)
assert.NoError(t, err)
- assert.Len(t, emails, 2)
+ assert.Len(t, emails, 3)
}
func TestReplacePrimaryEmailAddress(t *testing.T) {
diff --git a/services/user/user.go b/services/user/user.go
index 8ffd1e98a9..bb4c7dbee7 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -11,7 +11,6 @@ import (
"time"
"code.gitea.io/gitea/models"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
@@ -25,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/agit"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
org_service "code.gitea.io/gitea/services/org"
"code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
@@ -127,7 +127,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
return fmt.Errorf("%s is an organization not a user", u.Name)
}
- if user_model.IsLastAdminUser(ctx, u) {
+ if u.IsActive && user_model.IsLastAdminUser(ctx, u) {
return models.ErrDeleteLastAdminUser{UID: u.ID}
}
@@ -189,7 +189,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
break
}
for _, org := range orgs {
- if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
+ if err := models.RemoveOrgUser(ctx, org, u); err != nil {
if organization.IsErrLastOrgOwner(err) {
err = org_service.DeleteOrganization(ctx, org, true)
if err != nil {
@@ -251,59 +251,54 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
if err := committer.Commit(); err != nil {
return err
}
- committer.Close()
+ _ = committer.Close()
- if err = asymkey_model.RewriteAllPublicKeys(ctx); err != nil {
+ if err = asymkey_service.RewriteAllPublicKeys(ctx); err != nil {
return err
}
- if err = asymkey_model.RewriteAllPrincipalKeys(ctx); err != nil {
+ if err = asymkey_service.RewriteAllPrincipalKeys(ctx); err != nil {
return err
}
- // Note: There are something just cannot be roll back,
- // so just keep error logs of those operations.
+ // Note: There are something just cannot be roll back, so just keep error logs of those operations.
path := user_model.UserPath(u.Name)
- if err := util.RemoveAll(path); err != nil {
- err = fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
+ if err = util.RemoveAll(path); err != nil {
+ err = fmt.Errorf("failed to RemoveAll %s: %w", path, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
- return err
}
if u.Avatar != "" {
avatarPath := u.CustomAvatarRelativePath()
- if err := storage.Avatars.Delete(avatarPath); err != nil {
- err = fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
+ if err = storage.Avatars.Delete(avatarPath); err != nil {
+ err = fmt.Errorf("failed to remove %s: %w", avatarPath, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
- return err
}
}
return nil
}
-// DeleteInactiveUsers deletes all inactive users and email addresses.
+// DeleteInactiveUsers deletes all inactive users and their email addresses.
func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
- users, err := user_model.GetInactiveUsers(ctx, olderThan)
+ inactiveUsers, err := user_model.GetInactiveUsers(ctx, olderThan)
if err != nil {
return err
}
// FIXME: should only update authorized_keys file once after all deletions.
- for _, u := range users {
- select {
- case <-ctx.Done():
- return db.ErrCancelledf("Before delete inactive user %s", u.Name)
- default:
- }
- if err := DeleteUser(ctx, u, false); err != nil {
- // Ignore users that were set inactive by admin.
- if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) ||
- models.IsErrUserOwnPackages(err) || models.IsErrDeleteLastAdminUser(err) {
+ for _, u := range inactiveUsers {
+ if err = DeleteUser(ctx, u, false); err != nil {
+ // Ignore inactive users that were ever active but then were set inactive by admin
+ if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
continue
}
- return err
+ select {
+ case <-ctx.Done():
+ return db.ErrCancelledf("when deleting inactive user %q", u.Name)
+ default:
+ return err
+ }
}
}
-
- return user_model.DeleteInactiveEmailAddresses(ctx)
+ return nil // TODO: there could be still inactive users left, and the number would increase gradually
}
diff --git a/services/user/user_test.go b/services/user/user_test.go
index 2ebcded925..bd6019a14f 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
"testing"
+ "time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/auth"
@@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
)
@@ -41,7 +43,8 @@ func TestDeleteUser(t *testing.T) {
orgUsers := make([]*organization.OrgUser, 0, 10)
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
for _, orgUser := range orgUsers {
- if err := models.RemoveOrgUser(db.DefaultContext, orgUser.OrgID, orgUser.UID); err != nil {
+ org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
+ if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
assert.True(t, organization.IsErrLastOrgOwner(err))
return
}
@@ -184,3 +187,26 @@ func TestCreateUser_Issue5882(t *testing.T) {
assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
}
}
+
+func TestDeleteInactiveUsers(t *testing.T) {
+ addUser := func(name, email string, createdUnix timeutil.TimeStamp, active bool) {
+ inactiveUser := &user_model.User{Name: name, LowerName: strings.ToLower(name), Email: email, CreatedUnix: createdUnix, IsActive: active}
+ _, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(inactiveUser)
+ assert.NoError(t, err)
+ inactiveUserEmail := &user_model.EmailAddress{UID: inactiveUser.ID, IsPrimary: true, Email: email, LowerEmail: strings.ToLower(email), IsActivated: active}
+ err = db.Insert(db.DefaultContext, inactiveUserEmail)
+ assert.NoError(t, err)
+ }
+ addUser("user-inactive-10", "user-inactive-10@test.com", timeutil.TimeStampNow().Add(-600), false)
+ addUser("user-inactive-5", "user-inactive-5@test.com", timeutil.TimeStampNow().Add(-300), false)
+ addUser("user-active-10", "user-active-10@test.com", timeutil.TimeStampNow().Add(-600), true)
+ addUser("user-active-5", "user-active-5@test.com", timeutil.TimeStampNow().Add(-300), true)
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-10"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+ assert.NoError(t, DeleteInactiveUsers(db.DefaultContext, 8*time.Minute))
+ unittest.AssertNotExistsBean(t, &user_model.User{Name: "user-inactive-10"})
+ unittest.AssertNotExistsBean(t, &user_model.EmailAddress{Email: "user-inactive-10@test.com"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-inactive-5"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-10"})
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user-active-5"})
+}
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 8f728d3aa6..b2c0a73784 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -7,6 +7,7 @@ import (
"context"
"crypto/hmac"
"crypto/sha1"
+ "crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
@@ -29,39 +30,19 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/gobwas/glob"
- "github.com/minio/sha256-simd"
)
-// Deliver deliver hook task
-func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
- w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
- if err != nil {
- return err
- }
-
- defer func() {
- err := recover()
- if err == nil {
- return
- }
- // There was a panic whilst delivering a hook...
- log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
- }()
-
- t.IsDelivered = true
-
- var req *http.Request
-
+func newDefaultRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
switch w.HTTPMethod {
case "":
- log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL)
+ log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
fallthrough
case http.MethodPost:
switch w.ContentType {
case webhook_model.ContentTypeJSON:
req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
if err != nil {
- return err
+ return nil, nil, err
}
req.Header.Set("Content-Type", "application/json")
@@ -72,50 +53,58 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
if err != nil {
- return err
+ return nil, nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ default:
+ return nil, nil, fmt.Errorf("invalid content type: %v", w.ContentType)
}
case http.MethodGet:
u, err := url.Parse(w.URL)
if err != nil {
- return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err)
+ return nil, nil, fmt.Errorf("invalid URL: %w", err)
}
vals := u.Query()
vals["payload"] = []string{t.PayloadContent}
u.RawQuery = vals.Encode()
req, err = http.NewRequest("GET", u.String(), nil)
if err != nil {
- return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err)
+ return nil, nil, err
}
case http.MethodPut:
switch w.Type {
- case webhook_module.MATRIX:
+ case webhook_module.MATRIX: // used when t.Version == 1
txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
if err != nil {
- return err
+ return nil, nil, err
}
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
if err != nil {
- return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err)
+ return nil, nil, err
}
default:
- return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
+ return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
}
default:
- return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
+ return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
}
+ body = []byte(t.PayloadContent)
+ return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+}
+
+func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
var signatureSHA1 string
var signatureSHA256 string
- if len(w.Secret) > 0 {
- sig1 := hmac.New(sha1.New, []byte(w.Secret))
- sig256 := hmac.New(sha256.New, []byte(w.Secret))
- _, err = io.MultiWriter(sig1, sig256).Write([]byte(t.PayloadContent))
+ if len(secret) > 0 {
+ sig1 := hmac.New(sha1.New, secret)
+ sig256 := hmac.New(sha256.New, secret)
+ _, err := io.MultiWriter(sig1, sig256).Write(payloadContent)
if err != nil {
- log.Error("prepareWebhooks.sigWrite: %v", err)
+ // this error should never happen, since the hashes are writing to []byte and always return a nil error.
+ return fmt.Errorf("prepareWebhooks.sigWrite: %w", err)
}
signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
@@ -136,15 +125,36 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
req.Header["X-GitHub-Delivery"] = []string{t.UUID}
req.Header["X-GitHub-Event"] = []string{event}
req.Header["X-GitHub-Event-Type"] = []string{eventType}
+ return nil
+}
- // Add Authorization Header
- authorization, err := w.HeaderAuthorization()
+// Deliver creates the [http.Request] (depending on the webhook type), sends it
+// and records the status and response.
+func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
+ w, err := webhook_model.GetWebhookByID(ctx, t.HookID)
if err != nil {
- log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err)
return err
}
- if authorization != "" {
- req.Header["Authorization"] = []string{authorization}
+
+ defer func() {
+ err := recover()
+ if err == nil {
+ return
+ }
+ // There was a panic whilst delivering a hook...
+ log.Error("PANIC whilst trying to deliver webhook task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
+ }()
+
+ t.IsDelivered = true
+
+ newRequest := webhookRequesters[w.Type]
+ if t.PayloadVersion == 1 || newRequest == nil {
+ newRequest = newDefaultRequest
+ }
+
+ req, body, err := newRequest(ctx, w, t)
+ if err != nil {
+ return fmt.Errorf("cannot create http request for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
}
// Record delivery information.
@@ -152,11 +162,22 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
URL: req.URL.String(),
HTTPMethod: req.Method,
Headers: map[string]string{},
+ Body: string(body),
}
for k, vals := range req.Header {
t.RequestInfo.Headers[k] = strings.Join(vals, ",")
}
+ // Add Authorization Header
+ authorization, err := w.HeaderAuthorization()
+ if err != nil {
+ return fmt.Errorf("cannot get Authorization header for webhook %s[%d %s]: %w", w.Type, w.ID, w.URL, err)
+ }
+ if authorization != "" {
+ req.Header.Set("Authorization", authorization)
+ t.RequestInfo.Headers["Authorization"] = "******"
+ }
+
t.ResponseInfo = &webhook_model.HookResponse{
Headers: map[string]string{},
}
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index 72aa00478a..d0cfc1598f 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -5,9 +5,11 @@ package webhook
import (
"context"
+ "io"
"net/http"
"net/http/httptest"
"net/url"
+ "strings"
"testing"
"time"
@@ -16,7 +18,7 @@ import (
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/hostmatcher"
"code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
@@ -105,15 +107,16 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
assert.NoError(t, err)
assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
- db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true)
- hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush, Payloader: &api.PushPayload{}}
+ hookTask := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadVersion: 2,
+ }
hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
assert.NoError(t, err)
- if !assert.NotNil(t, hookTask) {
- return
- }
+ assert.NotNil(t, hookTask)
assert.NoError(t, Deliver(context.Background(), hookTask))
select {
@@ -123,4 +126,171 @@ func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
}
assert.True(t, hookTask.IsSucceed)
+ assert.Equal(t, "******", hookTask.RequestInfo.Headers["Authorization"])
+}
+
+func TestWebhookDeliverHookTask(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ done := make(chan struct{}, 1)
+ s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "PUT", r.Method)
+ switch r.URL.Path {
+ case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
+ // Version 1
+ assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
+ assert.Equal(t, "", r.Header.Get("Content-Type"))
+ body, err := io.ReadAll(r.Body)
+ assert.NoError(t, err)
+ assert.Equal(t, `{"data": 42}`, string(body))
+
+ case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
+ // Version 2
+ assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
+ assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
+ body, err := io.ReadAll(r.Body)
+ assert.NoError(t, err)
+ assert.Len(t, body, 2147)
+
+ default:
+ w.WriteHeader(404)
+ t.Fatalf("unexpected url path %s", r.URL.Path)
+ return
+ }
+ w.WriteHeader(200)
+ done <- struct{}{}
+ }))
+ t.Cleanup(s.Close)
+
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.MATRIX,
+ URL: s.URL + "/webhook",
+ HTTPMethod: "PUT",
+ ContentType: webhook_model.ContentTypeJSON,
+ Meta: `{"message_type":0}`, // text
+ }
+ assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
+
+ t.Run("Version 1", func(t *testing.T) {
+ hookTask := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: `{"data": 42}`,
+ PayloadVersion: 1,
+ }
+
+ hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+ assert.NoError(t, err)
+ assert.NotNil(t, hookTask)
+
+ assert.NoError(t, Deliver(context.Background(), hookTask))
+ select {
+ case <-done:
+ case <-time.After(5 * time.Second):
+ t.Fatal("waited to long for request to happen")
+ }
+
+ assert.True(t, hookTask.IsSucceed)
+ })
+
+ t.Run("Version 2", func(t *testing.T) {
+ p := pushTestPayload()
+ data, err := p.JSONPayload()
+ assert.NoError(t, err)
+
+ hookTask := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+ assert.NoError(t, err)
+ assert.NotNil(t, hookTask)
+
+ assert.NoError(t, Deliver(context.Background(), hookTask))
+ select {
+ case <-done:
+ case <-time.After(5 * time.Second):
+ t.Fatal("waited to long for request to happen")
+ }
+
+ assert.True(t, hookTask.IsSucceed)
+ })
+}
+
+func TestWebhookDeliverSpecificTypes(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ type hookCase struct {
+ gotBody chan []byte
+ httpMethod string // default to POST
+ }
+
+ cases := map[string]*hookCase{
+ webhook_module.SLACK: {},
+ webhook_module.DISCORD: {},
+ webhook_module.DINGTALK: {},
+ webhook_module.TELEGRAM: {},
+ webhook_module.MSTEAMS: {},
+ webhook_module.FEISHU: {},
+ webhook_module.MATRIX: {httpMethod: "PUT"},
+ webhook_module.WECHATWORK: {},
+ webhook_module.PACKAGIST: {},
+ }
+
+ s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ typ := strings.Split(r.URL.Path, "/")[1] // URL: "/{webhook_type}/other-path"
+ assert.Equal(t, "application/json", r.Header.Get("Content-Type"), r.URL.Path)
+ assert.Equal(t, util.IfZero(cases[typ].httpMethod, "POST"), r.Method, "webhook test request %q", r.URL.Path)
+ body, _ := io.ReadAll(r.Body) // read request and send it back to the test by testcase's chan
+ cases[typ].gotBody <- body
+ w.WriteHeader(http.StatusNoContent)
+ }))
+ t.Cleanup(s.Close)
+
+ p := pushTestPayload()
+ data, err := p.JSONPayload()
+ assert.NoError(t, err)
+
+ for typ := range cases {
+ cases[typ].gotBody = make(chan []byte, 1)
+ t.Run(typ, func(t *testing.T) {
+ t.Parallel()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: typ,
+ URL: s.URL + "/" + typ,
+ Meta: "{}",
+ }
+ assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
+
+ hookTask := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ hookTask, err := webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+ assert.NoError(t, err)
+ assert.NotNil(t, hookTask)
+
+ assert.NoError(t, Deliver(context.Background(), hookTask))
+
+ select {
+ case gotBody := <-cases[typ].gotBody:
+ assert.NotEqual(t, string(data), string(gotBody), "request body must be different from the event payload")
+ assert.Equal(t, hookTask.RequestInfo.Body, string(gotBody), "delivered webhook payload doesn't match saved request")
+ case <-time.After(5 * time.Second):
+ t.Fatal("waited to long for request to happen")
+ }
+
+ assert.True(t, hookTask.IsSucceed)
+ })
+ }
}
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index d615e7254f..c57d04415a 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -4,12 +4,14 @@
package webhook
import (
+ "context"
"fmt"
+ "net/http"
"net/url"
"strings"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -22,19 +24,8 @@ type (
DingtalkPayload dingtalk.Payload
)
-var _ PayloadConvertor = &DingtalkPayload{}
-
-// JSONPayload Marshals the DingtalkPayload to json
-func (d *DingtalkPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(d, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
// Create implements PayloadConvertor Create method
-func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -43,7 +34,7 @@ func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}
// Delete implements PayloadConvertor Delete method
-func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -52,14 +43,14 @@ func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}
// Fork implements PayloadConvertor Fork method
-func (d *DingtalkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
return createDingtalkPayload(title, title, fmt.Sprintf("view forked repo %s", p.Repo.FullName), p.Repo.HTMLURL), nil
}
// Push implements PayloadConvertor Push method
-func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
@@ -100,14 +91,14 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}
// Issue implements PayloadConvertor Issue method
-func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) {
text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view issue", p.Issue.HTMLURL), nil
}
// Wiki implements PayloadConvertor Wiki method
-func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Wiki(p *api.WikiPayload) (DingtalkPayload, error) {
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
@@ -115,27 +106,27 @@ func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
}
// IssueComment implements PayloadConvertor IssueComment method
-func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) IssueComment(p *api.IssueCommentPayload) (DingtalkPayload, error) {
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+p.Comment.Body, "view issue comment", p.Comment.HTMLURL), nil
}
// PullRequest implements PayloadConvertor PullRequest method
-func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) {
text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view pull request", p.PullRequest.HTMLURL), nil
}
// Review implements PayloadConvertor Review method
-func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (dc dingtalkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DingtalkPayload, error) {
var text, title string
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return DingtalkPayload{}, err
}
title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -146,14 +137,14 @@ func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module
}
// Repository implements PayloadConvertor Repository method
-func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayload, error) {
switch p.Action {
case api.HookRepoCreated:
title := fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil
case api.HookRepoDeleted:
title := fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
- return &DingtalkPayload{
+ return DingtalkPayload{
MsgType: "text",
Text: struct {
Content string `json:"content"`
@@ -163,24 +154,24 @@ func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
}, nil
}
- return nil, nil
+ return DingtalkPayload{}, nil
}
// Release implements PayloadConvertor Release method
-func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Release(p *api.ReleasePayload) (DingtalkPayload, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
}
-func (d *DingtalkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
}
-func createDingtalkPayload(title, text, singleTitle, singleURL string) *DingtalkPayload {
- return &DingtalkPayload{
+func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
+ return DingtalkPayload{
MsgType: "actionCard",
ActionCard: dingtalk.ActionCard{
Text: strings.TrimSpace(text),
@@ -195,7 +186,10 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk
}
}
-// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
-func GetDingtalkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
- return convertPayloader(new(DingtalkPayload), p, event)
+type dingtalkConvertor struct{}
+
+var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
+
+func newDingtalkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ return newJSONRequest(dingtalkConvertor{}, w, t, true)
}
diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go
index a03fa46f14..25f47347d0 100644
--- a/services/webhook/dingtalk_test.go
+++ b/services/webhook/dingtalk_test.go
@@ -4,9 +4,12 @@
package webhook
import (
+ "context"
"net/url"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -24,248 +27,226 @@ func TestDingTalkPayload(t *testing.T) {
}
return ""
}
+ dc := dingtalkConvertor{}
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.Create(p)
+ pl, err := dc.Create(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] branch test created", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] branch test created", pl.ActionCard.Title)
+ assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.Delete(p)
+ pl, err := dc.Delete(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] branch test deleted", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view ref test", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] branch test deleted", pl.ActionCard.Title)
+ assert.Equal(t, "view ref test", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.Fork(p)
+ pl, err := dc.Fork(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view forked repo test/repo", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Text)
+ assert.Equal(t, "test/repo2 is forked to test/repo", pl.ActionCard.Title)
+ assert.Equal(t, "view forked repo test/repo", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.Push(p)
+ pl, err := dc.Push(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view commits", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo:test] 2 new commits", pl.ActionCard.Title)
+ assert.Equal(t, "view commits", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(DingtalkPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := dc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.ActionCard.Text)
+ assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+ assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = dc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view issue", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.ActionCard.Text)
+ assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+ assert.Equal(t, "view issue", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.IssueComment(p)
+ pl, err := dc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.ActionCard.Text)
+ assert.Equal(t, "#2 crash", pl.ActionCard.Title)
+ assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.PullRequest(p)
+ pl, err := dc.PullRequest(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.ActionCard.Text)
+ assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
+ assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.IssueComment(p)
+ pl, err := dc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "#12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view issue comment", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.ActionCard.Text)
+ assert.Equal(t, "#12 Fix bug", pl.ActionCard.Title)
+ assert.Equal(t, "view issue comment", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(DingtalkPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view pull request", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug", pl.ActionCard.Title)
+ assert.Equal(t, "view pull request", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.Repository(p)
+ pl, err := dc.Repository(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] Repository created", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view repository", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] Repository created", pl.ActionCard.Title)
+ assert.Equal(t, "view repository", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.Package(p)
+ pl, err := dc.Package(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view package", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Text)
+ assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.ActionCard.Title)
+ assert.Equal(t, "view package", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(DingtalkPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := dc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.ActionCard.Title)
+ assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = dc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.ActionCard.Title)
+ assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = dc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.ActionCard.Title)
+ assert.Equal(t, "view wiki", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.ActionCard.SingleURL))
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(DingtalkPayload)
- pl, err := d.Release(p)
+ pl, err := dc.Release(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Text)
- assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*DingtalkPayload).ActionCard.Title)
- assert.Equal(t, "view release", pl.(*DingtalkPayload).ActionCard.SingleTitle)
- assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Text)
+ assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.ActionCard.Title)
+ assert.Equal(t, "view release", pl.ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", parseRealSingleURL(pl.ActionCard.SingleURL))
})
}
func TestDingTalkJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(DingtalkPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DingtalkPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.DINGTALK,
+ URL: "https://dingtalk.example.com/",
+ Meta: ``,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newDingtalkRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://dingtalk.example.com/", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body DingtalkPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.ActionCard.Text)
}
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index e2ac1410b8..659754d5e0 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -4,8 +4,10 @@
package webhook
import (
+ "context"
"errors"
"fmt"
+ "net/http"
"net/url"
"strconv"
"strings"
@@ -98,19 +100,8 @@ var (
redColor = color("ff3232")
)
-// JSONPayload Marshals the DiscordPayload to json
-func (d *DiscordPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(d, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
-var _ PayloadConvertor = &DiscordPayload{}
-
// Create implements PayloadConvertor Create method
-func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -119,7 +110,7 @@ func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}
// Delete implements PayloadConvertor Delete method
-func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (d discordConvertor) Delete(p *api.DeletePayload) (DiscordPayload, error) {
// deleted tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -128,14 +119,14 @@ func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}
// Fork implements PayloadConvertor Fork method
-func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (d discordConvertor) Fork(p *api.ForkPayload) (DiscordPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil
}
// Push implements PayloadConvertor Push method
-func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
@@ -170,35 +161,35 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}
// Issue implements PayloadConvertor Issue method
-func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) {
title, _, text, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, title, text, p.Issue.HTMLURL, color), nil
}
// IssueComment implements PayloadConvertor IssueComment method
-func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (d discordConvertor) IssueComment(p *api.IssueCommentPayload) (DiscordPayload, error) {
title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil
}
// PullRequest implements PayloadConvertor PullRequest method
-func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (d discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) {
title, _, text, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
}
// Review implements PayloadConvertor Review method
-func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (d discordConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DiscordPayload, error) {
var text, title string
var color int
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return DiscordPayload{}, err
}
title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -220,7 +211,7 @@ func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.
}
// Repository implements PayloadConvertor Repository method
-func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) {
var title, url string
var color int
switch p.Action {
@@ -237,7 +228,7 @@ func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
}
// Wiki implements PayloadConvertor Wiki method
-func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) {
text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
@@ -250,30 +241,35 @@ func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
}
// Release implements PayloadConvertor Release method
-func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (d discordConvertor) Release(p *api.ReleasePayload) (DiscordPayload, error) {
text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
}
-func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) {
text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
}
-// GetDiscordPayload converts a discord webhook into a DiscordPayload
-func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
- s := new(DiscordPayload)
+type discordConvertor struct {
+ Username string
+ AvatarURL string
+}
- discord := &DiscordMeta{}
- if err := json.Unmarshal([]byte(meta), &discord); err != nil {
- return s, errors.New("GetDiscordPayload meta json:" + err.Error())
+var _ payloadConvertor[DiscordPayload] = discordConvertor{}
+
+func newDiscordRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ meta := &DiscordMeta{}
+ if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+ return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err)
}
- s.Username = discord.Username
- s.AvatarURL = discord.IconURL
-
- return convertPayloader(s, p, event)
+ sc := discordConvertor{
+ Username: meta.Username,
+ AvatarURL: meta.IconURL,
+ }
+ return newJSONRequest(sc, w, t, true)
}
func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
@@ -291,8 +287,8 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
}
}
-func (d *DiscordPayload) createPayload(s *api.User, title, text, url string, color int) *DiscordPayload {
- return &DiscordPayload{
+func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload {
+ return DiscordPayload{
Username: d.Username,
AvatarURL: d.AvatarURL,
Embeds: []DiscordEmbed{
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index b567cbc395..c04b95383b 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -4,8 +4,11 @@
package webhook
import (
+ "context"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -15,295 +18,274 @@ import (
)
func TestDiscordPayload(t *testing.T) {
+ dc := discordConvertor{}
+
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(DiscordPayload)
- pl, err := d.Create(p)
+ pl, err := dc.Create(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] branch test created", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] branch test created", pl.Embeds[0].Title)
+ assert.Empty(t, pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(DiscordPayload)
- pl, err := d.Delete(p)
+ pl, err := dc.Delete(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] branch test deleted", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] branch test deleted", pl.Embeds[0].Title)
+ assert.Empty(t, pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(DiscordPayload)
- pl, err := d.Fork(p)
+ pl, err := dc.Fork(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "test/repo2 is forked to test/repo", pl.Embeds[0].Title)
+ assert.Empty(t, pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(DiscordPayload)
- pl, err := d.Push(p)
+ pl, err := dc.Push(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo:test] 2 new commits", pl.Embeds[0].Title)
+ assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(DiscordPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := dc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "issue body", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Embeds[0].Title)
+ assert.Equal(t, "issue body", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = dc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Embeds[0].Title)
+ assert.Empty(t, pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(DiscordPayload)
- pl, err := d.IssueComment(p)
+ pl, err := dc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "more info needed", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Embeds[0].Title)
+ assert.Equal(t, "more info needed", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(DiscordPayload)
- pl, err := d.PullRequest(p)
+ pl, err := dc.PullRequest(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "fixes bug #2", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Embeds[0].Title)
+ assert.Equal(t, "fixes bug #2", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(DiscordPayload)
- pl, err := d.IssueComment(p)
+ pl, err := dc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "changes requested", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Embeds[0].Title)
+ assert.Equal(t, "changes requested", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(DiscordPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := dc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "good job", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Embeds[0].Title)
+ assert.Equal(t, "good job", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(DiscordPayload)
- pl, err := d.Repository(p)
+ pl, err := dc.Repository(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Repository created", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Repository created", pl.Embeds[0].Title)
+ assert.Empty(t, pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(DiscordPayload)
- pl, err := d.Package(p)
+ pl, err := dc.Package(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "Package created: GiteaContainer:latest", pl.Embeds[0].Title)
+ assert.Empty(t, pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(DiscordPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := dc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Embeds[0].Title)
+ assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = dc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Embeds[0].Title)
+ assert.Equal(t, "Wiki change comment", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = dc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Embeds[0].Title)
+ assert.Empty(t, pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(DiscordPayload)
- pl, err := d.Release(p)
+ pl, err := dc.Release(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
- assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*DiscordPayload).Embeds[0].Title)
- assert.Equal(t, "Note of first stable release", pl.(*DiscordPayload).Embeds[0].Description)
- assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*DiscordPayload).Embeds[0].URL)
- assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
- assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
- assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ assert.Len(t, pl.Embeds, 1)
+ assert.Equal(t, "[test/repo] Release created: v1.0", pl.Embeds[0].Title)
+ assert.Equal(t, "Note of first stable release", pl.Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.Embeds[0].Author.IconURL)
})
}
func TestDiscordJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(DiscordPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &DiscordPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.DISCORD,
+ URL: "https://discord.example.com/",
+ Meta: `{}`,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newDiscordRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://discord.example.com/", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body DiscordPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Embeds[0].Description)
}
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 556443e70b..1ec436894b 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -4,11 +4,13 @@
package webhook
import (
+ "context"
"fmt"
+ "net/http"
"strings"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
)
@@ -23,8 +25,8 @@ type (
}
)
-func newFeishuTextPayload(text string) *FeishuPayload {
- return &FeishuPayload{
+func newFeishuTextPayload(text string) FeishuPayload {
+ return FeishuPayload{
MsgType: "text",
Content: struct {
Text string `json:"text"`
@@ -34,19 +36,8 @@ func newFeishuTextPayload(text string) *FeishuPayload {
}
}
-// JSONPayload Marshals the FeishuPayload to json
-func (f *FeishuPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(f, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
-var _ PayloadConvertor = &FeishuPayload{}
-
// Create implements PayloadConvertor Create method
-func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Create(p *api.CreatePayload) (FeishuPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
text := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -55,7 +46,7 @@ func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}
// Delete implements PayloadConvertor Delete method
-func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Delete(p *api.DeletePayload) (FeishuPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
text := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -64,14 +55,14 @@ func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}
// Fork implements PayloadConvertor Fork method
-func (f *FeishuPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Fork(p *api.ForkPayload) (FeishuPayload, error) {
text := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
return newFeishuTextPayload(text), nil
}
// Push implements PayloadConvertor Push method
-func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
@@ -96,48 +87,40 @@ func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}
// Issue implements PayloadConvertor Issue method
-func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) {
title, link, by, operator, result, assignees := getIssuesInfo(p)
- var res api.Payloader
if assignees != "" {
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
- res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body))
- } else {
- res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body))
+ return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)), nil
}
- } else {
- res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body))
+ return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)), nil
}
- return res, nil
+ return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)), nil
}
// IssueComment implements PayloadConvertor IssueComment method
-func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (fc feishuConvertor) IssueComment(p *api.IssueCommentPayload) (FeishuPayload, error) {
title, link, by, operator := getIssuesCommentInfo(p)
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil
}
// PullRequest implements PayloadConvertor PullRequest method
-func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) {
title, link, by, operator, result, assignees := getPullRequestInfo(p)
- var res api.Payloader
if assignees != "" {
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
- res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body))
- } else {
- res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body))
+ return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)), nil
}
- } else {
- res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body))
+ return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)), nil
}
- return res, nil
+ return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)), nil
}
// Review implements PayloadConvertor Review method
-func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) {
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return FeishuPayload{}, err
}
title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -147,7 +130,7 @@ func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.H
}
// Repository implements PayloadConvertor Repository method
-func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) {
var text string
switch p.Action {
case api.HookRepoCreated:
@@ -158,30 +141,33 @@ func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
return newFeishuTextPayload(text), nil
}
- return nil, nil
+ return FeishuPayload{}, nil
}
// Wiki implements PayloadConvertor Wiki method
-func (f *FeishuPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) {
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
return newFeishuTextPayload(text), nil
}
// Release implements PayloadConvertor Release method
-func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
return newFeishuTextPayload(text), nil
}
-func (f *FeishuPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
return newFeishuTextPayload(text), nil
}
-// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
-func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
- return convertPayloader(new(FeishuPayload), p, event)
+type feishuConvertor struct{}
+
+var _ payloadConvertor[FeishuPayload] = feishuConvertor{}
+
+func newFeishuRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ return newJSONRequest(feishuConvertor{}, w, t, true)
}
diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go
index 98bc50dede..ef18333fd4 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -4,8 +4,11 @@
package webhook
import (
+ "context"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -14,199 +17,177 @@ import (
)
func TestFeishuPayload(t *testing.T) {
+ fc := feishuConvertor{}
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(FeishuPayload)
- pl, err := d.Create(p)
+ pl, err := fc.Create(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, `[test/repo] branch test created`, pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, `[test/repo] branch test created`, pl.Content.Text)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(FeishuPayload)
- pl, err := d.Delete(p)
+ pl, err := fc.Delete(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, `[test/repo] branch test deleted`, pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, `[test/repo] branch test deleted`, pl.Content.Text)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(FeishuPayload)
- pl, err := d.Fork(p)
+ pl, err := fc.Fork(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Content.Text)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(FeishuPayload)
- pl, err := d.Push(p)
+ pl, err := fc.Push(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Content.Text)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(FeishuPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := fc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = fc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.Content.Text)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(FeishuPayload)
- pl, err := d.IssueComment(p)
+ pl, err := fc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.Content.Text)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(FeishuPayload)
- pl, err := d.PullRequest(p)
+ pl, err := fc.PullRequest(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.Content.Text)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(FeishuPayload)
- pl, err := d.IssueComment(p)
+ pl, err := fc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.Content.Text)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(FeishuPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := fc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[test/repo] Pull request review approved : #12 Fix bug\r\n\r\ngood job", pl.Content.Text)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(FeishuPayload)
- pl, err := d.Repository(p)
+ pl, err := fc.Repository(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[test/repo] Repository created", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[test/repo] Repository created", pl.Content.Text)
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(FeishuPayload)
- pl, err := d.Package(p)
+ pl, err := fc.Package(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "Package created: GiteaContainer:latest by user1", pl.Content.Text)
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(FeishuPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := fc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.Content.Text)
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = fc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.Content.Text)
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = fc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.Content.Text)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(FeishuPayload)
- pl, err := d.Release(p)
+ pl, err := fc.Release(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*FeishuPayload).Content.Text)
+ assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.Content.Text)
})
}
func TestFeishuJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(FeishuPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &FeishuPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.FEISHU,
+ URL: "https://feishu.example.com/",
+ Meta: `{}`,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newFeishuRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://feishu.example.com/", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body FeishuPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text)
}
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 602d16ef39..0329804a8b 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -4,11 +4,12 @@
package webhook
import (
+ "bytes"
+ "context"
"crypto/sha1"
"encoding/hex"
- "errors"
"fmt"
- "html"
+ "net/http"
"net/url"
"regexp"
"strings"
@@ -23,6 +24,37 @@ import (
webhook_module "code.gitea.io/gitea/modules/webhook"
)
+func newMatrixRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ meta := &MatrixMeta{}
+ if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+ return nil, nil, fmt.Errorf("GetMatrixPayload meta json: %w", err)
+ }
+ mc := matrixConvertor{
+ MsgType: messageTypeText[meta.MessageType],
+ }
+ payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ body, err := json.MarshalIndent(payload, "", " ")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ txnID, err := getMatrixTxnID(body)
+ if err != nil {
+ return nil, nil, err
+ }
+ req, err := http.NewRequest(http.MethodPut, w.URL+"/"+txnID, bytes.NewReader(body))
+ if err != nil {
+ return nil, nil, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially
+}
+
const matrixPayloadSizeLimit = 1024 * 64
// MatrixMeta contains the Matrix metadata
@@ -46,8 +78,6 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
return s
}
-var _ PayloadConvertor = &MatrixPayload{}
-
// MatrixPayload contains payload for a Matrix room
type MatrixPayload struct {
Body string `json:"body"`
@@ -57,90 +87,79 @@ type MatrixPayload struct {
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
}
-// JSONPayload Marshals the MatrixPayload to json
-func (m *MatrixPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(m, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
+var _ payloadConvertor[MatrixPayload] = matrixConvertor{}
+
+type matrixConvertor struct {
+ MsgType string
}
-// MatrixLinkFormatter creates a link compatible with Matrix
-func MatrixLinkFormatter(url, text string) string {
- return fmt.Sprintf(`%s`, html.EscapeString(url), html.EscapeString(text))
+func (m matrixConvertor) newPayload(text string, commits ...*api.PayloadCommit) (MatrixPayload, error) {
+ return MatrixPayload{
+ Body: getMessageBody(text),
+ MsgType: m.MsgType,
+ Format: "org.matrix.custom.html",
+ FormattedBody: text,
+ Commits: commits,
+ }, nil
}
-// MatrixLinkToRef Matrix-formatter link to a repo ref
-func MatrixLinkToRef(repoURL, ref string) string {
- refName := git.RefName(ref).ShortName()
- switch {
- case strings.HasPrefix(ref, git.BranchPrefix):
- return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
- case strings.HasPrefix(ref, git.TagPrefix):
- return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
- default:
- return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
- }
-}
-
-// Create implements PayloadConvertor Create method
-func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
- repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+// Create implements payloadConvertor Create method
+func (m matrixConvertor) Create(p *api.CreatePayload) (MatrixPayload, error) {
+ repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
// Delete composes Matrix payload for delete a branch or tag.
-func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (m matrixConvertor) Delete(p *api.DeletePayload) (MatrixPayload, error) {
refName := git.RefName(p.Ref).ShortName()
- repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+ repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
// Fork composes Matrix payload for forked by a repository.
-func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
- baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
- forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+func (m matrixConvertor) Fork(p *api.ForkPayload) (MatrixPayload, error) {
+ baseLink := htmlLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+ forkLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-// Issue implements PayloadConvertor Issue method
-func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
- text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
+// Issue implements payloadConvertor Issue method
+func (m matrixConvertor) Issue(p *api.IssuePayload) (MatrixPayload, error) {
+ text, _, _, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-// IssueComment implements PayloadConvertor IssueComment method
-func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
- text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
+// IssueComment implements payloadConvertor IssueComment method
+func (m matrixConvertor) IssueComment(p *api.IssueCommentPayload) (MatrixPayload, error) {
+ text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-// Wiki implements PayloadConvertor Wiki method
-func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
- text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true)
+// Wiki implements payloadConvertor Wiki method
+func (m matrixConvertor) Wiki(p *api.WikiPayload) (MatrixPayload, error) {
+ text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-// Release implements PayloadConvertor Release method
-func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
- text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
+// Release implements payloadConvertor Release method
+func (m matrixConvertor) Release(p *api.ReleasePayload) (MatrixPayload, error) {
+ text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-// Push implements PayloadConvertor Push method
-func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+// Push implements payloadConvertor Push method
+func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) {
var commitDesc string
if p.TotalCommits == 1 {
@@ -149,13 +168,13 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
commitDesc = fmt.Sprintf("%d commits", p.TotalCommits)
}
- repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
+ repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s] %s pushed %s to %s:
", repoLink, p.Pusher.UserName, commitDesc, branchLink)
// for each commit, generate a new line text
for i, commit := range p.Commits {
- text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
+ text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
// add linebreak to each commit but the last
if i < len(p.Commits)-1 {
text += "
"
@@ -163,41 +182,41 @@ func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}
- return getMatrixPayload(text, p.Commits, m.MsgType), nil
+ return m.newPayload(text, p.Commits...)
}
-// PullRequest implements PayloadConvertor PullRequest method
-func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
- text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
+// PullRequest implements payloadConvertor PullRequest method
+func (m matrixConvertor) PullRequest(p *api.PullRequestPayload) (MatrixPayload, error) {
+ text, _, _, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-// Review implements PayloadConvertor Review method
-func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
- senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
+// Review implements payloadConvertor Review method
+func (m matrixConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MatrixPayload, error) {
+ senderLink := htmlLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
- titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title)
- repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
+ titleLink := htmlLinkFormatter(p.PullRequest.HTMLURL, title)
+ repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return MatrixPayload{}, err
}
text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
}
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-// Repository implements PayloadConvertor Repository method
-func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
- senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
- repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
+// Repository implements payloadConvertor Repository method
+func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, error) {
+ senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+ repoLink := htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string
switch p.Action {
@@ -206,13 +225,12 @@ func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
case api.HookRepoDeleted:
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
}
-
- return getMatrixPayload(text, nil, m.MsgType), nil
+ return m.newPayload(text)
}
-func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
- senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
- packageLink := MatrixLinkFormatter(p.Package.HTMLURL, p.Package.Name)
+func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
+ senderLink := htmlLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+ packageLink := htmlLinkFormatter(p.Package.HTMLURL, p.Package.Name)
var text string
switch p.Action {
@@ -222,31 +240,7 @@ func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink)
}
- return getMatrixPayload(text, nil, m.MsgType), nil
-}
-
-// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
-func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
- s := new(MatrixPayload)
-
- matrix := &MatrixMeta{}
- if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
- return s, errors.New("GetMatrixPayload meta json:" + err.Error())
- }
-
- s.MsgType = messageTypeText[matrix.MessageType]
-
- return convertPayloader(s, p, event)
-}
-
-func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload {
- p := MatrixPayload{}
- p.FormattedBody = text
- p.Body = getMessageBody(text)
- p.Format = "org.matrix.custom.html"
- p.MsgType = msgType
- p.Commits = commits
- return &p
+ return m.newPayload(text)
}
var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?)`)
@@ -271,3 +265,16 @@ func getMatrixTxnID(payload []byte) (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}
+
+// MatrixLinkToRef Matrix-formatter link to a repo ref
+func MatrixLinkToRef(repoURL, ref string) string {
+ refName := git.RefName(ref).ShortName()
+ switch {
+ case strings.HasPrefix(ref, git.BranchPrefix):
+ return htmlLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
+ case strings.HasPrefix(ref, git.TagPrefix):
+ return htmlLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
+ default:
+ return htmlLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
+ }
+}
diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go
index 99a22fbd7e..058f8e3c5f 100644
--- a/services/webhook/matrix_test.go
+++ b/services/webhook/matrix_test.go
@@ -4,8 +4,11 @@
package webhook
import (
+ "context"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -14,217 +17,213 @@ import (
)
func TestMatrixPayload(t *testing.T) {
+ mc := matrixConvertor{
+ MsgType: "m.text",
+ }
+
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(MatrixPayload)
- pl, err := d.Create(p)
+ pl, err := mc.Create(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo:test] branch created by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.Body)
+ assert.Equal(t, `[test/repo:test] branch created by user1`, pl.FormattedBody)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(MatrixPayload)
- pl, err := d.Delete(p)
+ pl, err := mc.Delete(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.Body)
+ assert.Equal(t, `[test/repo:test] branch deleted by user1`, pl.FormattedBody)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(MatrixPayload)
- pl, err := d.Fork(p)
+ pl, err := mc.Fork(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.Body)
+ assert.Equal(t, `test/repo2 is forked to test/repo`, pl.FormattedBody)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(MatrixPayload)
- pl, err := d.Push(p)
+ pl, err := mc.Push(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:
2020558: commit message - user1
2020558: commit message - user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.Body)
+ assert.Equal(t, `[test/repo] user1 pushed 2 commits to test:
2020558: commit message - user1
2020558: commit message - user1`, pl.FormattedBody)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(MatrixPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := mc.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] Issue opened: #2 crash by user1`, pl.FormattedBody)
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = mc.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.FormattedBody)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(MatrixPayload)
- pl, err := d.IssueComment(p)
+ pl, err := mc.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] New comment on issue #2 crash by user1`, pl.FormattedBody)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(MatrixPayload)
- pl, err := d.PullRequest(p)
+ pl, err := mc.PullRequest(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] Pull request opened: #12 Fix bug by user1`, pl.FormattedBody)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(MatrixPayload)
- pl, err := d.IssueComment(p)
+ pl, err := mc.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] New comment on pull request #12 Fix bug by user1`, pl.FormattedBody)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(MatrixPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] Pull request review approved: #12 Fix bug by user1`, pl.FormattedBody)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(MatrixPayload)
- pl, err := d.Repository(p)
+ pl, err := mc.Repository(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Repository created by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.Body)
+ assert.Equal(t, `[test/repo] Repository created by user1`, pl.FormattedBody)
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(MatrixPayload)
- pl, err := d.Package(p)
+ pl, err := mc.Package(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
- assert.Equal(t, `[GiteaContainer] Package published by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, `[[GiteaContainer](http://localhost:3000/user1/-/packages/container/GiteaContainer/latest)] Package published by [user1](https://try.gitea.io/user1)`, pl.Body)
+ assert.Equal(t, `[GiteaContainer] Package published by user1`, pl.FormattedBody)
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(MatrixPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := mc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.FormattedBody)
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = mc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.FormattedBody)
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = mc.Wiki(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.FormattedBody)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(MatrixPayload)
- pl, err := d.Release(p)
+ pl, err := mc.Release(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
- assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.(*MatrixPayload).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body)
+ assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.FormattedBody)
})
}
func TestMatrixJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(MatrixPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MatrixPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.MATRIX,
+ URL: "https://matrix.example.com/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message",
+ Meta: `{"message_type":0}`, // text
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newMatrixRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "PUT", req.Method)
+ assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path)
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body MatrixPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", body.Body)
}
func Test_getTxnID(t *testing.T) {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 37810b4cd3..99d0106184 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -4,12 +4,14 @@
package webhook
import (
+ "context"
"fmt"
+ "net/http"
"net/url"
"strings"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -56,19 +58,8 @@ type (
}
)
-// JSONPayload Marshals the MSTeamsPayload to json
-func (m *MSTeamsPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(m, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
-var _ PayloadConvertor = &MSTeamsPayload{}
-
// Create implements PayloadConvertor Create method
-func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -85,7 +76,7 @@ func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}
// Delete implements PayloadConvertor Delete method
-func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) {
// deleted tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -102,7 +93,7 @@ func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}
// Fork implements PayloadConvertor Fork method
-func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
return createMSTeamsPayload(
@@ -117,7 +108,7 @@ func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
}
// Push implements PayloadConvertor Push method
-func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
@@ -160,7 +151,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}
// Issue implements PayloadConvertor Issue method
-func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) {
title, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
return createMSTeamsPayload(
@@ -175,7 +166,7 @@ func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
}
// IssueComment implements PayloadConvertor IssueComment method
-func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) {
title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
return createMSTeamsPayload(
@@ -190,7 +181,7 @@ func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader
}
// PullRequest implements PayloadConvertor PullRequest method
-func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) {
title, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
return createMSTeamsPayload(
@@ -205,14 +196,14 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
}
// Review implements PayloadConvertor Review method
-func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) {
var text, title string
var color int
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return MSTeamsPayload{}, err
}
title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -242,7 +233,7 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.
}
// Repository implements PayloadConvertor Repository method
-func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) {
var title, url string
var color int
switch p.Action {
@@ -267,7 +258,7 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
}
// Wiki implements PayloadConvertor Wiki method
-func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) {
title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
return createMSTeamsPayload(
@@ -282,7 +273,7 @@ func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
}
// Release implements PayloadConvertor Release method
-func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) {
title, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
return createMSTeamsPayload(
@@ -296,7 +287,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
), nil
}
-func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) {
title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
return createMSTeamsPayload(
@@ -310,12 +301,7 @@ func (m *MSTeamsPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
), nil
}
-// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
-func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
- return convertPayloader(new(MSTeamsPayload), p, event)
-}
-
-func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) *MSTeamsPayload {
+func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
facts := make([]MSTeamsFact, 0, 2)
if r != nil {
facts = append(facts, MSTeamsFact{
@@ -327,7 +313,7 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
facts = append(facts, *fact)
}
- return &MSTeamsPayload{
+ return MSTeamsPayload{
Type: "MessageCard",
Context: "https://schema.org/extensions",
ThemeColor: fmt.Sprintf("%x", color),
@@ -356,3 +342,11 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
},
}
}
+
+type msteamsConvertor struct{}
+
+var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
+
+func newMSTeamsRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ return newJSONRequest(msteamsConvertor{}, w, t, true)
+}
diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go
index 8d1aed6040..01e08b918e 100644
--- a/services/webhook/msteams_test.go
+++ b/services/webhook/msteams_test.go
@@ -4,8 +4,11 @@
package webhook
import (
+ "context"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -14,22 +17,20 @@ import (
)
func TestMSTeamsPayload(t *testing.T) {
+ mc := msteamsConvertor{}
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.Create(p)
+ pl, err := mc.Create(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] branch test created", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] branch test created", pl.Title)
+ assert.Equal(t, "[test/repo] branch test created", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value)
} else if fact.Name == "branch:" {
@@ -38,27 +39,24 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.Delete(p)
+ pl, err := mc.Delete(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] branch test deleted", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] branch test deleted", pl.Title)
+ assert.Equal(t, "[test/repo] branch test deleted", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value)
} else if fact.Name == "branch:" {
@@ -67,27 +65,24 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.Fork(p)
+ pl, err := mc.Fork(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "test/repo2 is forked to test/repo", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "test/repo2 is forked to test/repo", pl.Title)
+ assert.Equal(t, "test/repo2 is forked to test/repo", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value)
} else if fact.Name == "Forkee:" {
@@ -96,27 +91,24 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.Push(p)
+ pl, err := mc.Push(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo:test] 2 new commits", pl.Title)
+ assert.Equal(t, "[test/repo:test] 2 new commits", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\n\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repo.FullName, fact.Value)
} else if fact.Name == "Commit count:" {
@@ -125,28 +117,25 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/src/test", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(MSTeamsPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := mc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "issue body", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Title)
+ assert.Equal(t, "[test/repo] Issue opened: #2 crash", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "issue body", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else if fact.Name == "Issue #:" {
@@ -155,23 +144,21 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = mc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Title)
+ assert.Equal(t, "[test/repo] Issue closed: #2 crash", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else if fact.Name == "Issue #:" {
@@ -180,27 +167,24 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.IssueComment(p)
+ pl, err := mc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "more info needed", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Title)
+ assert.Equal(t, "[test/repo] New comment on issue #2 crash", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "more info needed", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else if fact.Name == "Issue #:" {
@@ -209,27 +193,24 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/issues/2#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.PullRequest(p)
+ pl, err := mc.PullRequest(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "fixes bug #2", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Title)
+ assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "fixes bug #2", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else if fact.Name == "Pull request #:" {
@@ -238,27 +219,24 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.IssueComment(p)
+ pl, err := mc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "changes requested", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Title)
+ assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "changes requested", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else if fact.Name == "Issue #:" {
@@ -267,28 +245,25 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12#issuecomment-4", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(MSTeamsPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := mc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "good job", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Title)
+ assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "good job", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else if fact.Name == "Pull request #:" {
@@ -297,155 +272,139 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.Repository(p)
+ pl, err := mc.Repository(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Repository created", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Repository created", pl.Title)
+ assert.Equal(t, "[test/repo] Repository created", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 1)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.Package(p)
+ pl, err := mc.Package(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "Package created: GiteaContainer:latest", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 1)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "Package created: GiteaContainer:latest", pl.Title)
+ assert.Equal(t, "Package created: GiteaContainer:latest", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 1)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Package:" {
assert.Equal(t, p.Package.Name, fact.Value)
} else {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/user1/-/packages/container/GiteaContainer/latest", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(MSTeamsPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := mc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Title)
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = mc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Title)
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Equal(t, "", pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = mc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Title)
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.PotentialAction[0].Targets[0].URI)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(MSTeamsPayload)
- pl, err := d.Release(p)
+ pl, err := mc.Release(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Title)
- assert.Equal(t, "[test/repo] Release created: v1.0", pl.(*MSTeamsPayload).Summary)
- assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
- assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
- assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
- for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ assert.Equal(t, "[test/repo] Release created: v1.0", pl.Title)
+ assert.Equal(t, "[test/repo] Release created: v1.0", pl.Summary)
+ assert.Len(t, pl.Sections, 1)
+ assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.Sections[0].Text)
+ assert.Len(t, pl.Sections[0].Facts, 2)
+ for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
assert.Equal(t, p.Repository.FullName, fact.Value)
} else if fact.Name == "Tag:" {
@@ -454,21 +413,43 @@ func TestMSTeamsPayload(t *testing.T) {
t.Fail()
}
}
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
- assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
- assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ assert.Len(t, pl.PotentialAction, 1)
+ assert.Len(t, pl.PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/releases/tag/v1.0", pl.PotentialAction[0].Targets[0].URI)
})
}
func TestMSTeamsJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(MSTeamsPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &MSTeamsPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.MSTEAMS,
+ URL: "https://msteams.example.com/",
+ Meta: ``,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newMSTeamsRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://msteams.example.com/", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body MSTeamsPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "[test/repo:test] 2 new commits", body.Summary)
}
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index d9d3bc1dd5..2a8e663637 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -67,7 +67,7 @@ func (m *webhookNotifier) IssueClearLabels(ctx context.Context, doer *user_model
err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
Action: api.HookIssueLabelCleared,
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
})
@@ -168,7 +168,7 @@ func (m *webhookNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo
permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
apiIssue := &api.IssuePayload{
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
}
@@ -214,7 +214,7 @@ func (m *webhookNotifier) IssueChangeTitle(ctx context.Context, doer *user_model
From: oldTitle,
},
},
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
})
@@ -250,7 +250,7 @@ func (m *webhookNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode
} else {
apiIssue := &api.IssuePayload{
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
CommitID: commitID,
@@ -281,7 +281,7 @@ func (m *webhookNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{
Action: api.HookIssueOpened,
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, issue.Poster, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, issue.Poster, nil),
}); err != nil {
@@ -349,7 +349,7 @@ func (m *webhookNotifier) IssueChangeContent(ctx context.Context, doer *user_mod
From: oldContent,
},
},
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
})
@@ -384,7 +384,7 @@ func (m *webhookNotifier) UpdateComment(ctx context.Context, doer *user_model.Us
permission, _ := access_model.GetUserRepoPermission(ctx, c.Issue.Repo, doer)
if err := PrepareWebhooks(ctx, EventSource{Repository: c.Issue.Repo}, eventType, &api.IssueCommentPayload{
Action: api.HookIssueCommentEdited,
- Issue: convert.ToAPIIssue(ctx, c.Issue),
+ Issue: convert.ToAPIIssue(ctx, doer, c.Issue),
Comment: convert.ToAPIComment(ctx, c.Issue.Repo, c),
Changes: &api.ChangesPayload{
Body: &api.ChangesFromPayload{
@@ -412,7 +412,7 @@ func (m *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
permission, _ := access_model.GetUserRepoPermission(ctx, repo, doer)
if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, eventType, &api.IssueCommentPayload{
Action: api.HookIssueCommentCreated,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Comment: convert.ToAPIComment(ctx, repo, comment),
Repository: convert.ToRepo(ctx, repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
@@ -449,7 +449,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
permission, _ := access_model.GetUserRepoPermission(ctx, comment.Issue.Repo, doer)
if err := PrepareWebhooks(ctx, EventSource{Repository: comment.Issue.Repo}, eventType, &api.IssueCommentPayload{
Action: api.HookIssueCommentDeleted,
- Issue: convert.ToAPIIssue(ctx, comment.Issue),
+ Issue: convert.ToAPIIssue(ctx, doer, comment.Issue),
Comment: convert.ToAPIComment(ctx, comment.Issue.Repo, comment),
Repository: convert.ToRepo(ctx, comment.Issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
@@ -533,7 +533,7 @@ func (m *webhookNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
})
@@ -575,7 +575,7 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueMilestone, &api.IssuePayload{
Action: hookAction,
Index: issue.Index,
- Issue: convert.ToAPIIssue(ctx, issue),
+ Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
})
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 714a4c076e..7880d8b606 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -4,7 +4,9 @@
package webhook
import (
- "errors"
+ "context"
+ "fmt"
+ "net/http"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/json"
@@ -38,84 +40,85 @@ func GetPackagistHook(w *webhook_model.Webhook) *PackagistMeta {
return s
}
-// JSONPayload Marshals the PackagistPayload to json
-func (f *PackagistPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(f, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
-var _ PayloadConvertor = &PackagistPayload{}
-
// Create implements PayloadConvertor Create method
-func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Create(_ *api.CreatePayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// Delete implements PayloadConvertor Delete method
-func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Delete(_ *api.DeletePayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// Fork implements PayloadConvertor Fork method
-func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Fork(_ *api.ForkPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// Push implements PayloadConvertor Push method
-func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) {
- return f, nil
+// https://packagist.org/about
+func (pc packagistConvertor) Push(_ *api.PushPayload) (PackagistPayload, error) {
+ return PackagistPayload{
+ PackagistRepository: struct {
+ URL string `json:"url"`
+ }{
+ URL: pc.PackageURL,
+ },
+ }, nil
}
// Issue implements PayloadConvertor Issue method
-func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Issue(_ *api.IssuePayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// IssueComment implements PayloadConvertor IssueComment method
-func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) IssueComment(_ *api.IssueCommentPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// PullRequest implements PayloadConvertor PullRequest method
-func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) PullRequest(_ *api.PullRequestPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// Review implements PayloadConvertor Review method
-func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// Repository implements PayloadConvertor Repository method
-func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Repository(_ *api.RepositoryPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// Wiki implements PayloadConvertor Wiki method
-func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Wiki(_ *api.WikiPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
// Release implements PayloadConvertor Release method
-func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Release(_ *api.ReleasePayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
-func (f *PackagistPayload) Package(_ *api.PackagePayload) (api.Payloader, error) {
- return nil, nil
+func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
}
-// GetPackagistPayload converts a packagist webhook into a PackagistPayload
-func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
- s := new(PackagistPayload)
+type packagistConvertor struct {
+ PackageURL string
+}
- packagist := &PackagistMeta{}
- if err := json.Unmarshal([]byte(meta), &packagist); err != nil {
- return s, errors.New("GetPackagistPayload meta json:" + err.Error())
+var _ payloadConvertor[PackagistPayload] = packagistConvertor{}
+
+func newPackagistRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ meta := &PackagistMeta{}
+ if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+ return nil, nil, fmt.Errorf("newpackagistRequest meta json: %w", err)
}
- s.PackagistRepository.URL = packagist.PackageURL
- return convertPayloader(s, p, event)
+ pc := packagistConvertor{
+ PackageURL: meta.PackageURL,
+ }
+ return newJSONRequest(pc, w, t, true)
}
diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go
index 26d01b0555..e9b0695baa 100644
--- a/services/webhook/packagist_test.go
+++ b/services/webhook/packagist_test.go
@@ -4,8 +4,11 @@
package webhook
import (
+ "context"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -14,155 +17,199 @@ import (
)
func TestPackagistPayload(t *testing.T) {
+ pc := packagistConvertor{
+ PackageURL: "https://packagist.org/packages/example",
+ }
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(PackagistPayload)
- pl, err := d.Create(p)
+ pl, err := pc.Create(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(PackagistPayload)
- pl, err := d.Delete(p)
+ pl, err := pc.Delete(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(PackagistPayload)
- pl, err := d.Fork(p)
+ pl, err := pc.Fork(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(PackagistPayload)
- d.PackagistRepository.URL = "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN"
- pl, err := d.Push(p)
+ pl, err := pc.Push(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &PackagistPayload{}, pl)
- assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", pl.(*PackagistPayload).PackagistRepository.URL)
+ assert.Equal(t, "https://packagist.org/packages/example", pl.PackagistRepository.URL)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(PackagistPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := pc.Issue(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = pc.Issue(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(PackagistPayload)
- pl, err := d.IssueComment(p)
+ pl, err := pc.IssueComment(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(PackagistPayload)
- pl, err := d.PullRequest(p)
+ pl, err := pc.PullRequest(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(PackagistPayload)
- pl, err := d.IssueComment(p)
+ pl, err := pc.IssueComment(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(PackagistPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(PackagistPayload)
- pl, err := d.Repository(p)
+ pl, err := pc.Repository(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(PackagistPayload)
- pl, err := d.Package(p)
+ pl, err := pc.Package(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(PackagistPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := pc.Wiki(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = pc.Wiki(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = pc.Wiki(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(PackagistPayload)
- pl, err := d.Release(p)
+ pl, err := pc.Release(p)
require.NoError(t, err)
- require.Nil(t, pl)
+ require.Equal(t, pl, PackagistPayload{})
})
}
func TestPackagistJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(PackagistPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &PackagistPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.PACKAGIST,
+ URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
+ Meta: `{"package_url":"https://packagist.org/packages/example"}`,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body PackagistPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "https://packagist.org/packages/example", body.PackagistRepository.URL)
+}
+
+func TestPackagistEmptyPayload(t *testing.T) {
+ p := createTestPayload()
+ data, err := p.JSONPayload()
+ require.NoError(t, err)
+
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.PACKAGIST,
+ URL: "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN",
+ Meta: `{"package_url":"https://packagist.org/packages/example"}`,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventCreate,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newPackagistRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
+ require.NoError(t, err)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://packagist.org/api/update-package?username=THEUSERNAME&apiToken=TOPSECRETAPITOKEN", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body PackagistPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "", body.PackagistRepository.URL)
}
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index bd482c04ea..54a11a5868 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -4,58 +4,109 @@
package webhook
import (
+ "bytes"
+ "fmt"
+ "net/http"
+
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
)
-// PayloadConvertor defines the interface to convert system webhook payload to external payload
-type PayloadConvertor interface {
- api.Payloader
- Create(*api.CreatePayload) (api.Payloader, error)
- Delete(*api.DeletePayload) (api.Payloader, error)
- Fork(*api.ForkPayload) (api.Payloader, error)
- Issue(*api.IssuePayload) (api.Payloader, error)
- IssueComment(*api.IssueCommentPayload) (api.Payloader, error)
- Push(*api.PushPayload) (api.Payloader, error)
- PullRequest(*api.PullRequestPayload) (api.Payloader, error)
- Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error)
- Repository(*api.RepositoryPayload) (api.Payloader, error)
- Release(*api.ReleasePayload) (api.Payloader, error)
- Wiki(*api.WikiPayload) (api.Payloader, error)
- Package(*api.PackagePayload) (api.Payloader, error)
+// payloadConvertor defines the interface to convert system payload to webhook payload
+type payloadConvertor[T any] interface {
+ Create(*api.CreatePayload) (T, error)
+ Delete(*api.DeletePayload) (T, error)
+ Fork(*api.ForkPayload) (T, error)
+ Issue(*api.IssuePayload) (T, error)
+ IssueComment(*api.IssueCommentPayload) (T, error)
+ Push(*api.PushPayload) (T, error)
+ PullRequest(*api.PullRequestPayload) (T, error)
+ Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error)
+ Repository(*api.RepositoryPayload) (T, error)
+ Release(*api.ReleasePayload) (T, error)
+ Wiki(*api.WikiPayload) (T, error)
+ Package(*api.PackagePayload) (T, error)
}
-func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
+func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
+ var p P
+ if err := json.Unmarshal(data, &p); err != nil {
+ var t T
+ return t, fmt.Errorf("could not unmarshal payload: %w", err)
+ }
+ return convert(p)
+}
+
+func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) {
switch event {
case webhook_module.HookEventCreate:
- return s.Create(p.(*api.CreatePayload))
+ return convertUnmarshalledJSON(rc.Create, data)
case webhook_module.HookEventDelete:
- return s.Delete(p.(*api.DeletePayload))
+ return convertUnmarshalledJSON(rc.Delete, data)
case webhook_module.HookEventFork:
- return s.Fork(p.(*api.ForkPayload))
+ return convertUnmarshalledJSON(rc.Fork, data)
case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
- return s.Issue(p.(*api.IssuePayload))
+ return convertUnmarshalledJSON(rc.Issue, data)
case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
- pl, ok := p.(*api.IssueCommentPayload)
- if ok {
- return s.IssueComment(pl)
- }
- return s.PullRequest(p.(*api.PullRequestPayload))
+ // previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload))
+ // however I couldn't find in notifier.go such a payload with an HookEvent***Comment event
+
+ // History (most recent first):
+ // - refactored in https://github.com/go-gitea/gitea/pull/12310
+ // - assertion added in https://github.com/go-gitea/gitea/pull/12046
+ // - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996
+ // > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload
+
+ // In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload)
+ return convertUnmarshalledJSON(rc.IssueComment, data)
case webhook_module.HookEventPush:
- return s.Push(p.(*api.PushPayload))
+ return convertUnmarshalledJSON(rc.Push, data)
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
- return s.PullRequest(p.(*api.PullRequestPayload))
+ return convertUnmarshalledJSON(rc.PullRequest, data)
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
- return s.Review(p.(*api.PullRequestPayload), event)
+ return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) {
+ return rc.Review(p, event)
+ }, data)
case webhook_module.HookEventRepository:
- return s.Repository(p.(*api.RepositoryPayload))
+ return convertUnmarshalledJSON(rc.Repository, data)
case webhook_module.HookEventRelease:
- return s.Release(p.(*api.ReleasePayload))
+ return convertUnmarshalledJSON(rc.Release, data)
case webhook_module.HookEventWiki:
- return s.Wiki(p.(*api.WikiPayload))
+ return convertUnmarshalledJSON(rc.Wiki, data)
case webhook_module.HookEventPackage:
- return s.Package(p.(*api.PackagePayload))
+ return convertUnmarshalledJSON(rc.Package, data)
}
- return s, nil
+ var t T
+ return t, fmt.Errorf("newPayload unsupported event: %s", event)
+}
+
+func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
+ payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ body, err := json.MarshalIndent(payload, "", " ")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ method := w.HTTPMethod
+ if method == "" {
+ method = http.MethodPost
+ }
+
+ req, err := http.NewRequest(method, w.URL, bytes.NewReader(body))
+ if err != nil {
+ return nil, nil, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ if withDefaultHeaders {
+ return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
+ }
+ return req, body, nil
}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 945b0662d8..ba8bac27d9 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -4,8 +4,9 @@
package webhook
import (
- "errors"
+ "context"
"fmt"
+ "net/http"
"regexp"
"strings"
@@ -39,7 +40,6 @@ func GetSlackHook(w *webhook_model.Webhook) *SlackMeta {
type SlackPayload struct {
Channel string `json:"channel"`
Text string `json:"text"`
- Color string `json:"-"`
Username string `json:"username"`
IconURL string `json:"icon_url"`
UnfurlLinks int `json:"unfurl_links"`
@@ -56,15 +56,6 @@ type SlackAttachment struct {
Text string `json:"text"`
}
-// JSONPayload Marshals the SlackPayload to json
-func (s *SlackPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(s, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
// SlackTextFormatter replaces &, <, > with HTML characters
// see: https://api.slack.com/docs/formatting
func SlackTextFormatter(s string) string {
@@ -98,10 +89,8 @@ func SlackLinkToRef(repoURL, ref string) string {
return SlackLinkFormatter(url, refName)
}
-var _ PayloadConvertor = &SlackPayload{}
-
-// Create implements PayloadConvertor Create method
-func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+// Create implements payloadConvertor Create method
+func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) {
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
@@ -110,7 +99,7 @@ func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}
// Delete composes Slack payload for delete a branch or tag.
-func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (s slackConvertor) Delete(p *api.DeletePayload) (SlackPayload, error) {
refName := git.RefName(p.Ref).ShortName()
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
@@ -119,7 +108,7 @@ func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}
// Fork composes Slack payload for forked by a repository.
-func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (s slackConvertor) Fork(p *api.ForkPayload) (SlackPayload, error) {
baseLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
@@ -127,8 +116,8 @@ func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
return s.createPayload(text, nil), nil
}
-// Issue implements PayloadConvertor Issue method
-func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+// Issue implements payloadConvertor Issue method
+func (s slackConvertor) Issue(p *api.IssuePayload) (SlackPayload, error) {
text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true)
var attachments []SlackAttachment
@@ -146,8 +135,8 @@ func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
return s.createPayload(text, attachments), nil
}
-// IssueComment implements PayloadConvertor IssueComment method
-func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+// IssueComment implements payloadConvertor IssueComment method
+func (s slackConvertor) IssueComment(p *api.IssueCommentPayload) (SlackPayload, error) {
text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true)
return s.createPayload(text, []SlackAttachment{{
@@ -158,28 +147,28 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader,
}}), nil
}
-// Wiki implements PayloadConvertor Wiki method
-func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+// Wiki implements payloadConvertor Wiki method
+func (s slackConvertor) Wiki(p *api.WikiPayload) (SlackPayload, error) {
text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true)
return s.createPayload(text, nil), nil
}
-// Release implements PayloadConvertor Release method
-func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+// Release implements payloadConvertor Release method
+func (s slackConvertor) Release(p *api.ReleasePayload) (SlackPayload, error) {
text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true)
return s.createPayload(text, nil), nil
}
-func (s *SlackPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
text, _ := getPackagePayloadInfo(p, SlackLinkFormatter, true)
return s.createPayload(text, nil), nil
}
-// Push implements PayloadConvertor Push method
-func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+// Push implements payloadConvertor Push method
+func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
// n new commits
var (
commitDesc string
@@ -219,8 +208,8 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}}), nil
}
-// PullRequest implements PayloadConvertor PullRequest method
-func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+// PullRequest implements payloadConvertor PullRequest method
+func (s slackConvertor) PullRequest(p *api.PullRequestPayload) (SlackPayload, error) {
text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true)
var attachments []SlackAttachment
@@ -238,8 +227,8 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er
return s.createPayload(text, attachments), nil
}
-// Review implements PayloadConvertor Review method
-func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+// Review implements payloadConvertor Review method
+func (s slackConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (SlackPayload, error) {
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
@@ -250,7 +239,7 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return SlackPayload{}, err
}
text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink)
@@ -259,8 +248,8 @@ func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.Ho
return s.createPayload(text, nil), nil
}
-// Repository implements PayloadConvertor Repository method
-func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+// Repository implements payloadConvertor Repository method
+func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, error) {
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string
@@ -275,8 +264,8 @@ func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, erro
return s.createPayload(text, nil), nil
}
-func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment) *SlackPayload {
- return &SlackPayload{
+func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
+ return SlackPayload{
Channel: s.Channel,
Text: text,
Username: s.Username,
@@ -285,21 +274,27 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment)
}
}
-// GetSlackPayload converts a slack webhook into a SlackPayload
-func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
- s := new(SlackPayload)
+type slackConvertor struct {
+ Channel string
+ Username string
+ IconURL string
+ Color string
+}
- slack := &SlackMeta{}
- if err := json.Unmarshal([]byte(meta), &slack); err != nil {
- return s, errors.New("GetSlackPayload meta json:" + err.Error())
+var _ payloadConvertor[SlackPayload] = slackConvertor{}
+
+func newSlackRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ meta := &SlackMeta{}
+ if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
+ return nil, nil, fmt.Errorf("newSlackRequest meta json: %w", err)
}
-
- s.Channel = slack.Channel
- s.Username = slack.Username
- s.IconURL = slack.IconURL
- s.Color = slack.Color
-
- return convertPayloader(s, p, event)
+ sc := slackConvertor{
+ Channel: meta.Channel,
+ Username: meta.Username,
+ IconURL: meta.IconURL,
+ Color: meta.Color,
+ }
+ return newJSONRequest(sc, w, t, true)
}
var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go
index b1340963e2..7ebf16aba2 100644
--- a/services/webhook/slack_test.go
+++ b/services/webhook/slack_test.go
@@ -4,8 +4,11 @@
package webhook
import (
+ "context"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -14,201 +17,180 @@ import (
)
func TestSlackPayload(t *testing.T) {
+ sc := slackConvertor{}
+
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(SlackPayload)
- pl, err := d.Create(p)
+ pl, err := sc.Create(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[:] branch created by user1", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[:] branch created by user1", pl.Text)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(SlackPayload)
- pl, err := d.Delete(p)
+ pl, err := sc.Delete(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[:test] branch deleted by user1", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[:test] branch deleted by user1", pl.Text)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(SlackPayload)
- pl, err := d.Fork(p)
+ pl, err := sc.Fork(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, " is forked to ", pl.(*SlackPayload).Text)
+ assert.Equal(t, " is forked to ", pl.Text)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(SlackPayload)
- pl, err := d.Push(p)
+ pl, err := sc.Push(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[:] 2 new commits pushed by user1", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[:] 2 new commits pushed by user1", pl.Text)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(SlackPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := sc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Issue opened: by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Issue opened: by ", pl.Text)
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = sc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Issue closed: by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Issue closed: by ", pl.Text)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(SlackPayload)
- pl, err := d.IssueComment(p)
+ pl, err := sc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] New comment on issue by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] New comment on issue by ", pl.Text)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(SlackPayload)
- pl, err := d.PullRequest(p)
+ pl, err := sc.PullRequest(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Pull request opened: by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Pull request opened: by ", pl.Text)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(SlackPayload)
- pl, err := d.IssueComment(p)
+ pl, err := sc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] New comment on pull request by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] New comment on pull request by ", pl.Text)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(SlackPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := sc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by ", pl.Text)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(SlackPayload)
- pl, err := d.Repository(p)
+ pl, err := sc.Repository(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Repository created by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Repository created by ", pl.Text)
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(SlackPayload)
- pl, err := d.Package(p)
+ pl, err := sc.Package(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "Package created: by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "Package created: by ", pl.Text)
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(SlackPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := sc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] New wiki page '' (Wiki change comment) by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] New wiki page '' (Wiki change comment) by ", pl.Text)
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = sc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Wiki page '' edited (Wiki change comment) by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Wiki page '' edited (Wiki change comment) by ", pl.Text)
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = sc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Wiki page '' deleted by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Wiki page '' deleted by ", pl.Text)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(SlackPayload)
- pl, err := d.Release(p)
+ pl, err := sc.Release(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Release created: by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Release created: by ", pl.Text)
})
}
func TestSlackJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(SlackPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &SlackPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.SLACK,
+ URL: "https://slack.example.com/",
+ Meta: `{}`,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newSlackRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://slack.example.com/", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body SlackPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "[:] 2 new commits pushed by user1", body.Text)
}
func TestIsValidSlackChannel(t *testing.T) {
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 1bdc74e183..c2b4820032 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -4,14 +4,15 @@
package webhook
import (
+ "context"
"fmt"
+ "net/http"
"strings"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
)
@@ -41,22 +42,8 @@ func GetTelegramHook(w *webhook_model.Webhook) *TelegramMeta {
return s
}
-var _ PayloadConvertor = &TelegramPayload{}
-
-// JSONPayload Marshals the TelegramPayload to json
-func (t *TelegramPayload) JSONPayload() ([]byte, error) {
- t.ParseMode = "HTML"
- t.DisableWebPreview = true
- t.Message = markup.Sanitize(t.Message)
- data, err := json.MarshalIndent(t, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
// Create implements PayloadConvertor Create method
-func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (t telegramConvertor) Create(p *api.CreatePayload) (TelegramPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf(`[%s] %s %s created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType,
@@ -66,7 +53,7 @@ func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
}
// Delete implements PayloadConvertor Delete method
-func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (t telegramConvertor) Delete(p *api.DeletePayload) (TelegramPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf(`[%s] %s %s deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType,
@@ -76,14 +63,14 @@ func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
}
// Fork implements PayloadConvertor Fork method
-func (t *TelegramPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (t telegramConvertor) Fork(p *api.ForkPayload) (TelegramPayload, error) {
title := fmt.Sprintf(`%s is forked to %s`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName)
return createTelegramPayload(title), nil
}
// Push implements PayloadConvertor Push method
-func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
@@ -121,34 +108,34 @@ func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}
// Issue implements PayloadConvertor Issue method
-func (t *TelegramPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (t telegramConvertor) Issue(p *api.IssuePayload) (TelegramPayload, error) {
text, _, attachmentText, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true)
return createTelegramPayload(text + "\n\n" + attachmentText), nil
}
// IssueComment implements PayloadConvertor IssueComment method
-func (t *TelegramPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (t telegramConvertor) IssueComment(p *api.IssueCommentPayload) (TelegramPayload, error) {
text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true)
return createTelegramPayload(text + "\n" + p.Comment.Body), nil
}
// PullRequest implements PayloadConvertor PullRequest method
-func (t *TelegramPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (t telegramConvertor) PullRequest(p *api.PullRequestPayload) (TelegramPayload, error) {
text, _, attachmentText, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true)
return createTelegramPayload(text + "\n" + attachmentText), nil
}
// Review implements PayloadConvertor Review method
-func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (t telegramConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (TelegramPayload, error) {
var text, attachmentText string
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return TelegramPayload{}, err
}
text = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
@@ -159,7 +146,7 @@ func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module
}
// Repository implements PayloadConvertor Repository method
-func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload, error) {
var title string
switch p.Action {
case api.HookRepoCreated:
@@ -169,36 +156,41 @@ func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
return createTelegramPayload(title), nil
}
- return nil, nil
+ return TelegramPayload{}, nil
}
// Wiki implements PayloadConvertor Wiki method
-func (t *TelegramPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (t telegramConvertor) Wiki(p *api.WikiPayload) (TelegramPayload, error) {
text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
return createTelegramPayload(text), nil
}
// Release implements PayloadConvertor Release method
-func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (t telegramConvertor) Release(p *api.ReleasePayload) (TelegramPayload, error) {
text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
return createTelegramPayload(text), nil
}
-func (t *TelegramPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, error) {
text, _ := getPackagePayloadInfo(p, htmlLinkFormatter, true)
return createTelegramPayload(text), nil
}
-// GetTelegramPayload converts a telegram webhook into a TelegramPayload
-func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
- return convertPayloader(new(TelegramPayload), p, event)
-}
-
-func createTelegramPayload(message string) *TelegramPayload {
- return &TelegramPayload{
- Message: strings.TrimSpace(message),
+func createTelegramPayload(message string) TelegramPayload {
+ return TelegramPayload{
+ Message: strings.TrimSpace(message),
+ ParseMode: "HTML",
+ DisableWebPreview: true,
}
}
+
+type telegramConvertor struct{}
+
+var _ payloadConvertor[TelegramPayload] = telegramConvertor{}
+
+func newTelegramRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ return newJSONRequest(telegramConvertor{}, w, t, true)
+}
diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go
index 5b9927d057..2fe5161b22 100644
--- a/services/webhook/telegram_test.go
+++ b/services/webhook/telegram_test.go
@@ -4,8 +4,11 @@
package webhook
import (
+ "context"
"testing"
+ webhook_model "code.gitea.io/gitea/models/webhook"
+ "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
@@ -14,199 +17,186 @@ import (
)
func TestTelegramPayload(t *testing.T) {
+ tc := telegramConvertor{}
+
+ t.Run("Correct webhook params", func(t *testing.T) {
+ p := createTelegramPayload("testMsg ")
+
+ assert.Equal(t, "HTML", p.ParseMode)
+ assert.Equal(t, true, p.DisableWebPreview)
+ assert.Equal(t, "testMsg", p.Message)
+ })
+
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(TelegramPayload)
- pl, err := d.Create(p)
+ pl, err := tc.Create(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] branch test created`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] branch test created`, pl.Message)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(TelegramPayload)
- pl, err := d.Delete(p)
+ pl, err := tc.Delete(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] branch test deleted`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] branch test deleted`, pl.Message)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(TelegramPayload)
- pl, err := d.Fork(p)
+ pl, err := tc.Fork(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `test/repo2 is forked to test/repo`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `test/repo2 is forked to test/repo`, pl.Message)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(TelegramPayload)
- pl, err := d.Push(p)
+ pl, err := tc.Push(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", pl.(*TelegramPayload).Message)
+ assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", pl.Message)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(TelegramPayload)
p.Action = api.HookIssueOpened
- pl, err := d.Issue(p)
+ pl, err := tc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\n\nissue body", pl.(*TelegramPayload).Message)
+ assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\n\nissue body", pl.Message)
p.Action = api.HookIssueClosed
- pl, err = d.Issue(p)
+ pl, err = tc.Issue(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] Issue closed: #2 crash by user1`, pl.Message)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(TelegramPayload)
- pl, err := d.IssueComment(p)
+ pl, err := tc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\nmore info needed", pl.(*TelegramPayload).Message)
+ assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1\nmore info needed", pl.Message)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(TelegramPayload)
- pl, err := d.PullRequest(p)
+ pl, err := tc.PullRequest(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\nfixes bug #2", pl.(*TelegramPayload).Message)
+ assert.Equal(t, "[test/repo] Pull request opened: #12 Fix bug by user1\nfixes bug #2", pl.Message)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(TelegramPayload)
- pl, err := d.IssueComment(p)
+ pl, err := tc.IssueComment(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\nchanges requested", pl.(*TelegramPayload).Message)
+ assert.Equal(t, "[test/repo] New comment on pull request #12 Fix bug by user1\nchanges requested", pl.Message)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(TelegramPayload)
- pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
+ pl, err := tc.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.(*TelegramPayload).Message)
+ assert.Equal(t, "[test/repo] Pull request review approved: #12 Fix bug\ngood job", pl.Message)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(TelegramPayload)
- pl, err := d.Repository(p)
+ pl, err := tc.Repository(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] Repository created`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] Repository created`, pl.Message)
})
t.Run("Package", func(t *testing.T) {
p := packageTestPayload()
- d := new(TelegramPayload)
- pl, err := d.Package(p)
+ pl, err := tc.Package(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `Package created: GiteaContainer:latest by user1`, pl.Message)
})
t.Run("Wiki", func(t *testing.T) {
p := wikiTestPayload()
- d := new(TelegramPayload)
p.Action = api.HookWikiCreated
- pl, err := d.Wiki(p)
+ pl, err := tc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] New wiki page 'index' (Wiki change comment) by user1`, pl.Message)
p.Action = api.HookWikiEdited
- pl, err = d.Wiki(p)
+ pl, err = tc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] Wiki page 'index' edited (Wiki change comment) by user1`, pl.Message)
p.Action = api.HookWikiDeleted
- pl, err = d.Wiki(p)
+ pl, err = tc.Wiki(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] Wiki page 'index' deleted by user1`, pl.Message)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(TelegramPayload)
- pl, err := d.Release(p)
+ pl, err := tc.Release(p)
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo] Release created: v1.0 by user1`, pl.Message)
})
}
func TestTelegramJSONPayload(t *testing.T) {
p := pushTestPayload()
-
- pl, err := new(TelegramPayload).Push(p)
+ data, err := p.JSONPayload()
require.NoError(t, err)
- require.NotNil(t, pl)
- require.IsType(t, &TelegramPayload{}, pl)
- json, err := pl.JSONPayload()
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ IsActive: true,
+ Type: webhook_module.TELEGRAM,
+ URL: "https://telegram.example.com/",
+ Meta: ``,
+ HTTPMethod: "POST",
+ }
+ task := &webhook_model.HookTask{
+ HookID: hook.ID,
+ EventType: webhook_module.HookEventPush,
+ PayloadContent: string(data),
+ PayloadVersion: 2,
+ }
+
+ req, reqBody, err := newTelegramRequest(context.Background(), hook, task)
+ require.NotNil(t, req)
+ require.NotNil(t, reqBody)
require.NoError(t, err)
- assert.NotEmpty(t, json)
+
+ assert.Equal(t, "POST", req.Method)
+ assert.Equal(t, "https://telegram.example.com/", req.URL.String())
+ assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
+ assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+ var body TelegramPayload
+ err = json.NewDecoder(req.Body).Decode(&body)
+ assert.NoError(t, err)
+ assert.Equal(t, "[test/repo:test] 2 new commits\n[2020558] commit message - user1\n[2020558] commit message - user1", body.Message)
}
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index ac18da3525..e0e8fa2fc1 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
+ "net/http"
"strings"
"code.gitea.io/gitea/models/db"
@@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -25,48 +27,16 @@ import (
"github.com/gobwas/glob"
)
-type webhook struct {
- name webhook_module.HookType
- payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error)
-}
-
-var webhooks = map[webhook_module.HookType]*webhook{
- webhook_module.SLACK: {
- name: webhook_module.SLACK,
- payloadCreator: GetSlackPayload,
- },
- webhook_module.DISCORD: {
- name: webhook_module.DISCORD,
- payloadCreator: GetDiscordPayload,
- },
- webhook_module.DINGTALK: {
- name: webhook_module.DINGTALK,
- payloadCreator: GetDingtalkPayload,
- },
- webhook_module.TELEGRAM: {
- name: webhook_module.TELEGRAM,
- payloadCreator: GetTelegramPayload,
- },
- webhook_module.MSTEAMS: {
- name: webhook_module.MSTEAMS,
- payloadCreator: GetMSTeamsPayload,
- },
- webhook_module.FEISHU: {
- name: webhook_module.FEISHU,
- payloadCreator: GetFeishuPayload,
- },
- webhook_module.MATRIX: {
- name: webhook_module.MATRIX,
- payloadCreator: GetMatrixPayload,
- },
- webhook_module.WECHATWORK: {
- name: webhook_module.WECHATWORK,
- payloadCreator: GetWechatworkPayload,
- },
- webhook_module.PACKAGIST: {
- name: webhook_module.PACKAGIST,
- payloadCreator: GetPackagistPayload,
- },
+var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){
+ webhook_module.SLACK: newSlackRequest,
+ webhook_module.DISCORD: newDiscordRequest,
+ webhook_module.DINGTALK: newDingtalkRequest,
+ webhook_module.TELEGRAM: newTelegramRequest,
+ webhook_module.MSTEAMS: newMSTeamsRequest,
+ webhook_module.FEISHU: newFeishuRequest,
+ webhook_module.MATRIX: newMatrixRequest,
+ webhook_module.WECHATWORK: newWechatworkRequest,
+ webhook_module.PACKAGIST: newPackagistRequest,
}
// IsValidHookTaskType returns true if a webhook registered
@@ -74,7 +44,7 @@ func IsValidHookTaskType(name string) bool {
if name == webhook_module.GITEA || name == webhook_module.GOGS {
return true
}
- _, ok := webhooks[name]
+ _, ok := webhookRequesters[name]
return ok
}
@@ -158,7 +128,9 @@ func checkBranch(w *webhook_model.Webhook, branch string) bool {
return g.Match(branch)
}
-// PrepareWebhook creates a hook task and enqueues it for processing
+// PrepareWebhook creates a hook task and enqueues it for processing.
+// The payload is saved as-is. The adjustments depending on the webhook type happen
+// right before delivery, in the [Deliver] method.
func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
// Skip sending if webhooks are disabled.
if setting.DisableWebhooks {
@@ -192,25 +164,19 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
}
}
- var payloader api.Payloader
- var err error
- webhook, ok := webhooks[w.Type]
- if ok {
- payloader, err = webhook.payloadCreator(p, event, w.Meta)
- if err != nil {
- return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err)
- }
- } else {
- payloader = p
+ payload, err := p.JSONPayload()
+ if err != nil {
+ return fmt.Errorf("JSONPayload for %s: %w", event, err)
}
task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
- HookID: w.ID,
- Payloader: payloader,
- EventType: event,
+ HookID: w.ID,
+ PayloadContent: string(payload),
+ EventType: event,
+ PayloadVersion: 2,
})
if err != nil {
- return fmt.Errorf("CreateHookTask: %w", err)
+ return fmt.Errorf("CreateHookTask for %s: %w", event, err)
}
return enqueueHookTask(task.ID)
@@ -225,7 +191,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
if source.Repository != nil {
repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
RepoID: source.Repository.ID,
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
})
if err != nil {
return fmt.Errorf("ListWebhooksByOpts: %w", err)
@@ -239,7 +205,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
if owner != nil {
ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
OwnerID: owner.ID,
- IsActive: util.OptionalBoolTrue,
+ IsActive: optional.Some(true),
})
if err != nil {
return fmt.Errorf("ListWebhooksByOpts: %w", err)
@@ -248,7 +214,7 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu
}
// Add any admin-defined system webhooks
- systemHooks, err := webhook_model.GetSystemWebhooks(ctx, util.OptionalBoolTrue)
+ systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true))
if err != nil {
return fmt.Errorf("GetSystemWebhooks: %w", err)
}
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 338b94360b..5f5c146232 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -77,7 +77,3 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
unittest.AssertNotExistsBean(t, hookTask)
}
}
-
-// TODO TestHookTask_deliver
-
-// TODO TestDeliverHooks
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 80245c7e77..46e7856ecf 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -4,11 +4,13 @@
package webhook
import (
+ "context"
"fmt"
+ "net/http"
"strings"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
)
@@ -28,20 +30,8 @@ type (
}
)
-// SetSecret sets the Wechatwork secret
-func (f *WechatworkPayload) SetSecret(_ string) {}
-
-// JSONPayload Marshals the WechatworkPayload to json
-func (f *WechatworkPayload) JSONPayload() ([]byte, error) {
- data, err := json.MarshalIndent(f, "", " ")
- if err != nil {
- return []byte{}, err
- }
- return data, nil
-}
-
-func newWechatworkMarkdownPayload(title string) *WechatworkPayload {
- return &WechatworkPayload{
+func newWechatworkMarkdownPayload(title string) WechatworkPayload {
+ return WechatworkPayload{
Msgtype: "markdown",
Markdown: struct {
Content string `json:"content"`
@@ -51,10 +41,8 @@ func newWechatworkMarkdownPayload(title string) *WechatworkPayload {
}
}
-var _ PayloadConvertor = &WechatworkPayload{}
-
// Create implements PayloadConvertor Create method
-func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Create(p *api.CreatePayload) (WechatworkPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
@@ -63,7 +51,7 @@ func (f *WechatworkPayload) Create(p *api.CreatePayload) (api.Payloader, error)
}
// Delete implements PayloadConvertor Delete method
-func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Delete(p *api.DeletePayload) (WechatworkPayload, error) {
// created tag/branch
refName := git.RefName(p.Ref).ShortName()
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
@@ -72,14 +60,14 @@ func (f *WechatworkPayload) Delete(p *api.DeletePayload) (api.Payloader, error)
}
// Fork implements PayloadConvertor Fork method
-func (f *WechatworkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Fork(p *api.ForkPayload) (WechatworkPayload, error) {
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
return newWechatworkMarkdownPayload(title), nil
}
// Push implements PayloadConvertor Push method
-func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error) {
var (
branchName = git.RefName(p.Ref).ShortName()
commitDesc string
@@ -108,7 +96,7 @@ func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) {
}
// Issue implements PayloadConvertor Issue method
-func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Issue(p *api.IssuePayload) (WechatworkPayload, error) {
text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
var content string
content += fmt.Sprintf(" >%s\n >%s \n > %s \n [%s](%s)", text, attachmentText, issueTitle, p.Issue.HTMLURL, p.Issue.HTMLURL)
@@ -117,7 +105,7 @@ func (f *WechatworkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
}
// IssueComment implements PayloadConvertor IssueComment method
-func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) IssueComment(p *api.IssueCommentPayload) (WechatworkPayload, error) {
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
var content string
content += fmt.Sprintf(" >%s\n >%s \n >%s \n [%s](%s)", text, p.Comment.Body, issueTitle, p.Comment.HTMLURL, p.Comment.HTMLURL)
@@ -126,7 +114,7 @@ func (f *WechatworkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloa
}
// PullRequest implements PayloadConvertor PullRequest method
-func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) PullRequest(p *api.PullRequestPayload) (WechatworkPayload, error) {
text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
pr := fmt.Sprintf("> %s \r\n > %s \r\n > %s \r\n",
text, issueTitle, attachmentText)
@@ -135,13 +123,13 @@ func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloade
}
// Review implements PayloadConvertor Review method
-func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
+func (wc wechatworkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (WechatworkPayload, error) {
var text, title string
switch p.Action {
case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
- return nil, err
+ return WechatworkPayload{}, err
}
title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
text = p.Review.Content
@@ -151,7 +139,7 @@ func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_modu
}
// Repository implements PayloadConvertor Repository method
-func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPayload, error) {
var title string
switch p.Action {
case api.HookRepoCreated:
@@ -162,30 +150,33 @@ func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader,
return newWechatworkMarkdownPayload(title), nil
}
- return nil, nil
+ return WechatworkPayload{}, nil
}
// Wiki implements PayloadConvertor Wiki method
-func (f *WechatworkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Wiki(p *api.WikiPayload) (WechatworkPayload, error) {
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
return newWechatworkMarkdownPayload(text), nil
}
// Release implements PayloadConvertor Release method
-func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Release(p *api.ReleasePayload) (WechatworkPayload, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
return newWechatworkMarkdownPayload(text), nil
}
-func (f *WechatworkPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
+func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, error) {
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
return newWechatworkMarkdownPayload(text), nil
}
-// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
-func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
- return convertPayloader(new(WechatworkPayload), p, event)
+type wechatworkConvertor struct{}
+
+var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
+
+func newWechatworkRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
+ return newJSONRequest(wechatworkConvertor{}, w, t, true)
}
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 6392d4ce83..8221e297d9 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -6,10 +6,12 @@ package wiki
import (
"context"
+ "errors"
"fmt"
"os"
"strings"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit"
@@ -19,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/sync"
+ "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -26,10 +29,7 @@ import (
// TODO: use clustered lock (unique queue? or *abuse* cache)
var wikiWorkingPool = sync.NewExclusivePool()
-const (
- DefaultRemote = "origin"
- DefaultBranch = "master"
-)
+const DefaultRemote = "origin"
// InitWiki initializes a wiki for repository,
// it does nothing when repository already has wiki.
@@ -42,25 +42,25 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
return fmt.Errorf("InitRepository: %w", err)
} else if err = gitrepo.CreateDelegateHooks(ctx, repo, true); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err)
- } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
- return fmt.Errorf("unable to set default wiki branch to master: %w", err)
+ } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + repo.DefaultWikiBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
+ return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err)
}
return nil
}
// prepareGitPath try to find a suitable file path with file name by the given raw wiki name.
// return: existence, prepared file path with name, error
-func prepareGitPath(gitRepo *git.Repository, wikiPath WebPath) (bool, string, error) {
+func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
unescaped := string(wikiPath) + ".md"
gitPath := WebPathToGitPath(wikiPath)
// Look for both files
- filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, gitPath)
+ filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath)
if err != nil {
- if strings.Contains(err.Error(), "Not a valid object name master") {
- return false, gitPath, nil
+ if strings.Contains(err.Error(), "Not a valid object name") {
+ return false, gitPath, nil // branch doesn't exist
}
- log.Error("%v", err)
+ log.Error("Wiki LsTree failed, err: %v", err)
return false, gitPath, err
}
@@ -96,7 +96,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
return fmt.Errorf("InitWiki: %w", err)
}
- hasMasterBranch := gitrepo.IsWikiBranchExist(ctx, repo, DefaultBranch)
+ hasDefaultBranch := gitrepo.IsWikiBranchExist(ctx, repo, repo.DefaultWikiBranch)
basePath, err := repo_module.CreateTemporaryPath("update-wiki")
if err != nil {
@@ -113,8 +113,8 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
Shared: true,
}
- if hasMasterBranch {
- cloneOpts.Branch = DefaultBranch
+ if hasDefaultBranch {
+ cloneOpts.Branch = repo.DefaultWikiBranch
}
if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil {
@@ -129,14 +129,14 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
}
defer gitRepo.Close()
- if hasMasterBranch {
+ if hasDefaultBranch {
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err)
}
}
- isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, newWikiName)
+ isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, newWikiName)
if err != nil {
return err
}
@@ -152,7 +152,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
isOldWikiExist := true
oldWikiPath := newWikiPath
if oldWikiName != newWikiName {
- isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, oldWikiName)
+ isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.DefaultWikiBranch, oldWikiName)
if err != nil {
return err
}
@@ -161,7 +161,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
if isOldWikiExist {
err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
if err != nil {
- log.Error("%v", err)
+ log.Error("RemoveFilesFromIndex failed: %v", err)
return err
}
}
@@ -171,18 +171,18 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
objectHash, err := gitRepo.HashObject(strings.NewReader(content))
if err != nil {
- log.Error("%v", err)
+ log.Error("HashObject failed: %v", err)
return err
}
if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
- log.Error("%v", err)
+ log.Error("AddObjectToIndex failed: %v", err)
return err
}
tree, err := gitRepo.WriteTree()
if err != nil {
- log.Error("%v", err)
+ log.Error("WriteTree failed: %v", err)
return err
}
@@ -201,19 +201,19 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
} else {
commitTreeOpts.NoGPGSign = true
}
- if hasMasterBranch {
+ if hasDefaultBranch {
commitTreeOpts.Parents = []string{"HEAD"}
}
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
if err != nil {
- log.Error("%v", err)
+ log.Error("CommitTree failed: %v", err)
return err
}
if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
Remote: DefaultRemote,
- Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
+ Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
Env: repo_module.FullPushingEnvironment(
doer,
doer,
@@ -222,11 +222,11 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
0,
),
}); err != nil {
- log.Error("%v", err)
+ log.Error("Push failed: %v", err)
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err
}
- return fmt.Errorf("Push: %w", err)
+ return fmt.Errorf("failed to push: %w", err)
}
return nil
@@ -270,7 +270,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
- Branch: DefaultBranch,
+ Branch: repo.DefaultWikiBranch,
}); err != nil {
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
@@ -288,7 +288,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err)
}
- found, wikiPath, err := prepareGitPath(gitRepo, wikiName)
+ found, wikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, wikiName)
if err != nil {
return err
}
@@ -332,7 +332,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
Remote: DefaultRemote,
- Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
+ Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
Env: repo_module.FullPushingEnvironment(
doer,
doer,
@@ -359,3 +359,41 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath())
return nil
}
+
+func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, newBranch string) error {
+ if !git.IsValidRefPattern(newBranch) {
+ return fmt.Errorf("invalid branch name: %s", newBranch)
+ }
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ repo.DefaultWikiBranch = newBranch
+ if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_wiki_branch"); err != nil {
+ return fmt.Errorf("unable to update database: %w", err)
+ }
+
+ if !repo.HasWiki() {
+ return nil
+ }
+
+ oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo)
+ if err != nil {
+ return fmt.Errorf("unable to get default branch: %w", err)
+ }
+ if oldDefBranch == newBranch {
+ return nil
+ }
+
+ gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo)
+ if errors.Is(err, util.ErrNotExist) {
+ return nil // no git repo on storage, no need to do anything else
+ } else if err != nil {
+ return fmt.Errorf("unable to open repository: %w", err)
+ }
+ defer gitRepo.Close()
+
+ err = gitRepo.RenameBranch(oldDefBranch, newBranch)
+ if err != nil {
+ return fmt.Errorf("unable to rename default branch: %w", err)
+ }
+ return nil
+ })
+}
diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go
index 59c77060f2..0a18cffa25 100644
--- a/services/wiki/wiki_test.go
+++ b/services/wiki/wiki_test.go
@@ -170,7 +170,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
return
}
defer gitRepo.Close()
- masterTree, err := gitRepo.GetTree(DefaultBranch)
+ masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
assert.NoError(t, err)
gitPath := WebPathToGitPath(webPath)
entry, err := masterTree.GetTreeEntryByPath(gitPath)
@@ -215,7 +215,7 @@ func TestRepository_EditWikiPage(t *testing.T) {
// Now need to show that the page has been added:
gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo)
assert.NoError(t, err)
- masterTree, err := gitRepo.GetTree(DefaultBranch)
+ masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
assert.NoError(t, err)
gitPath := WebPathToGitPath(webPath)
entry, err := masterTree.GetTreeEntryByPath(gitPath)
@@ -242,7 +242,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) {
return
}
defer gitRepo.Close()
- masterTree, err := gitRepo.GetTree(DefaultBranch)
+ masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
assert.NoError(t, err)
gitPath := WebPathToGitPath("Home")
_, err = masterTree.GetTreeEntryByPath(gitPath)
@@ -280,7 +280,7 @@ func TestPrepareWikiFileName(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
webPath := UserTitleToWebPath("", tt.arg)
- existence, newWikiPath, err := prepareGitPath(gitRepo, webPath)
+ existence, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, webPath)
if (err != nil) != tt.wantErr {
assert.NoError(t, err)
return
@@ -312,7 +312,7 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
}
defer gitRepo.Close()
- existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
+ existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home")
assert.False(t, existence)
assert.NoError(t, err)
assert.EqualValues(t, "Home.md", newWikiPath)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 7c10074bc5..4c09a9d588 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -44,7 +44,7 @@ parts:
source: .
stage-packages: [ git, sqlite3, openssh-client ]
build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential]
- build-snaps: [ go/1.21/stable, node/18/stable ]
+ build-snaps: [ go/1.22/stable, node/20/stable ]
build-environment:
- LDFLAGS: ""
override-pull: |
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100644
index 0000000000..523b18841e
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,246 @@
+import {fileURLToPath} from 'node:url';
+
+const cssVarFiles = [
+ fileURLToPath(new URL('web_src/css/base.css', import.meta.url)),
+ fileURLToPath(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url)),
+ fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
+];
+
+/** @type {import('stylelint').Config} */
+export default {
+ plugins: [
+ 'stylelint-declaration-strict-value',
+ 'stylelint-declaration-block-no-ignored-properties',
+ 'stylelint-value-no-unknown-custom-properties',
+ '@stylistic/stylelint-plugin',
+ ],
+ ignoreFiles: [
+ '**/*.go',
+ '/web_src/fomantic',
+ ],
+ overrides: [
+ {
+ files: ['**/chroma/*', '**/codemirror/*', '**/standalone/*', '**/console.css', 'font_i18n.css'],
+ rules: {
+ 'scale-unlimited/declaration-strict-value': null,
+ },
+ },
+ {
+ files: ['**/chroma/*', '**/codemirror/*'],
+ rules: {
+ 'block-no-empty': null,
+ },
+ },
+ {
+ files: ['**/*.vue'],
+ customSyntax: 'postcss-html',
+ },
+ ],
+ rules: {
+ '@stylistic/at-rule-name-case': null,
+ '@stylistic/at-rule-name-newline-after': null,
+ '@stylistic/at-rule-name-space-after': null,
+ '@stylistic/at-rule-semicolon-newline-after': null,
+ '@stylistic/at-rule-semicolon-space-before': null,
+ '@stylistic/block-closing-brace-empty-line-before': null,
+ '@stylistic/block-closing-brace-newline-after': null,
+ '@stylistic/block-closing-brace-newline-before': null,
+ '@stylistic/block-closing-brace-space-after': null,
+ '@stylistic/block-closing-brace-space-before': null,
+ '@stylistic/block-opening-brace-newline-after': null,
+ '@stylistic/block-opening-brace-newline-before': null,
+ '@stylistic/block-opening-brace-space-after': null,
+ '@stylistic/block-opening-brace-space-before': 'always',
+ '@stylistic/color-hex-case': 'lower',
+ '@stylistic/declaration-bang-space-after': 'never',
+ '@stylistic/declaration-bang-space-before': null,
+ '@stylistic/declaration-block-semicolon-newline-after': null,
+ '@stylistic/declaration-block-semicolon-newline-before': null,
+ '@stylistic/declaration-block-semicolon-space-after': null,
+ '@stylistic/declaration-block-semicolon-space-before': 'never',
+ '@stylistic/declaration-block-trailing-semicolon': null,
+ '@stylistic/declaration-colon-newline-after': null,
+ '@stylistic/declaration-colon-space-after': null,
+ '@stylistic/declaration-colon-space-before': 'never',
+ '@stylistic/function-comma-newline-after': null,
+ '@stylistic/function-comma-newline-before': null,
+ '@stylistic/function-comma-space-after': null,
+ '@stylistic/function-comma-space-before': null,
+ '@stylistic/function-max-empty-lines': 0,
+ '@stylistic/function-parentheses-newline-inside': 'never-multi-line',
+ '@stylistic/function-parentheses-space-inside': null,
+ '@stylistic/function-whitespace-after': null,
+ '@stylistic/indentation': 2,
+ '@stylistic/linebreaks': null,
+ '@stylistic/max-empty-lines': 1,
+ '@stylistic/max-line-length': null,
+ '@stylistic/media-feature-colon-space-after': null,
+ '@stylistic/media-feature-colon-space-before': 'never',
+ '@stylistic/media-feature-name-case': null,
+ '@stylistic/media-feature-parentheses-space-inside': null,
+ '@stylistic/media-feature-range-operator-space-after': 'always',
+ '@stylistic/media-feature-range-operator-space-before': 'always',
+ '@stylistic/media-query-list-comma-newline-after': null,
+ '@stylistic/media-query-list-comma-newline-before': null,
+ '@stylistic/media-query-list-comma-space-after': null,
+ '@stylistic/media-query-list-comma-space-before': null,
+ '@stylistic/named-grid-areas-alignment': null,
+ '@stylistic/no-empty-first-line': null,
+ '@stylistic/no-eol-whitespace': true,
+ '@stylistic/no-extra-semicolons': true,
+ '@stylistic/no-missing-end-of-source-newline': null,
+ '@stylistic/number-leading-zero': null,
+ '@stylistic/number-no-trailing-zeros': null,
+ '@stylistic/property-case': 'lower',
+ '@stylistic/selector-attribute-brackets-space-inside': null,
+ '@stylistic/selector-attribute-operator-space-after': null,
+ '@stylistic/selector-attribute-operator-space-before': null,
+ '@stylistic/selector-combinator-space-after': null,
+ '@stylistic/selector-combinator-space-before': null,
+ '@stylistic/selector-descendant-combinator-no-non-space': null,
+ '@stylistic/selector-list-comma-newline-after': null,
+ '@stylistic/selector-list-comma-newline-before': null,
+ '@stylistic/selector-list-comma-space-after': 'always-single-line',
+ '@stylistic/selector-list-comma-space-before': 'never-single-line',
+ '@stylistic/selector-max-empty-lines': 0,
+ '@stylistic/selector-pseudo-class-case': 'lower',
+ '@stylistic/selector-pseudo-class-parentheses-space-inside': 'never',
+ '@stylistic/selector-pseudo-element-case': 'lower',
+ '@stylistic/string-quotes': 'double',
+ '@stylistic/unicode-bom': null,
+ '@stylistic/unit-case': 'lower',
+ '@stylistic/value-list-comma-newline-after': null,
+ '@stylistic/value-list-comma-newline-before': null,
+ '@stylistic/value-list-comma-space-after': null,
+ '@stylistic/value-list-comma-space-before': null,
+ '@stylistic/value-list-max-empty-lines': 0,
+ 'alpha-value-notation': null,
+ 'annotation-no-unknown': true,
+ 'at-rule-allowed-list': null,
+ 'at-rule-disallowed-list': null,
+ 'at-rule-empty-line-before': null,
+ 'at-rule-no-unknown': [true, {ignoreAtRules: ['tailwind']}],
+ 'at-rule-no-vendor-prefix': true,
+ 'at-rule-property-required-list': null,
+ 'block-no-empty': true,
+ 'color-function-notation': null,
+ 'color-hex-alpha': null,
+ 'color-hex-length': null,
+ 'color-named': null,
+ 'color-no-hex': null,
+ 'color-no-invalid-hex': true,
+ 'comment-empty-line-before': null,
+ 'comment-no-empty': true,
+ 'comment-pattern': null,
+ 'comment-whitespace-inside': null,
+ 'comment-word-disallowed-list': null,
+ 'csstools/value-no-unknown-custom-properties': [true, {importFrom: cssVarFiles}],
+ 'custom-media-pattern': null,
+ 'custom-property-empty-line-before': null,
+ 'custom-property-no-missing-var-function': true,
+ 'custom-property-pattern': null,
+ 'declaration-block-no-duplicate-custom-properties': true,
+ 'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates-with-different-values']}],
+ 'declaration-block-no-redundant-longhand-properties': null,
+ 'declaration-block-no-shorthand-property-overrides': null,
+ 'declaration-block-single-line-max-declarations': null,
+ 'declaration-empty-line-before': null,
+ 'declaration-no-important': null,
+ 'declaration-property-max-values': null,
+ 'declaration-property-unit-allowed-list': null,
+ 'declaration-property-unit-disallowed-list': {'line-height': ['em']},
+ 'declaration-property-value-allowed-list': null,
+ 'declaration-property-value-disallowed-list': null,
+ 'declaration-property-value-no-unknown': true,
+ 'font-family-name-quotes': 'always-where-recommended',
+ 'font-family-no-duplicate-names': true,
+ 'font-family-no-missing-generic-family-keyword': true,
+ 'font-weight-notation': null,
+ 'function-allowed-list': null,
+ 'function-calc-no-unspaced-operator': true,
+ 'function-disallowed-list': null,
+ 'function-linear-gradient-no-nonstandard-direction': true,
+ 'function-name-case': 'lower',
+ 'function-no-unknown': true,
+ 'function-url-no-scheme-relative': null,
+ 'function-url-quotes': 'always',
+ 'function-url-scheme-allowed-list': null,
+ 'function-url-scheme-disallowed-list': null,
+ 'hue-degree-notation': null,
+ 'import-notation': 'string',
+ 'keyframe-block-no-duplicate-selectors': true,
+ 'keyframe-declaration-no-important': true,
+ 'keyframe-selector-notation': null,
+ 'keyframes-name-pattern': null,
+ 'length-zero-no-unit': [true, {ignore: ['custom-properties']}, {ignoreFunctions: ['var']}],
+ 'max-nesting-depth': null,
+ 'media-feature-name-allowed-list': null,
+ 'media-feature-name-disallowed-list': null,
+ 'media-feature-name-no-unknown': true,
+ 'media-feature-name-no-vendor-prefix': true,
+ 'media-feature-name-unit-allowed-list': null,
+ 'media-feature-name-value-allowed-list': null,
+ 'media-feature-name-value-no-unknown': true,
+ 'media-feature-range-notation': null,
+ 'media-query-no-invalid': true,
+ 'named-grid-areas-no-invalid': true,
+ 'no-descending-specificity': null,
+ 'no-duplicate-at-import-rules': true,
+ 'no-duplicate-selectors': true,
+ 'no-empty-source': true,
+ 'no-invalid-double-slash-comments': true,
+ 'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}],
+ 'no-irregular-whitespace': true,
+ 'no-unknown-animations': null,
+ 'no-unknown-custom-properties': null,
+ 'number-max-precision': null,
+ 'plugin/declaration-block-no-ignored-properties': true,
+ 'property-allowed-list': null,
+ 'property-disallowed-list': null,
+ 'property-no-unknown': true,
+ 'property-no-vendor-prefix': null,
+ 'rule-empty-line-before': null,
+ 'rule-selector-property-disallowed-list': null,
+ 'scale-unlimited/declaration-strict-value': [['/color$/', 'font-weight'], {ignoreValues: '/^(inherit|transparent|unset|initial|currentcolor|none)$/', ignoreFunctions: false, disableFix: true, expandShorthand: true}],
+ 'selector-anb-no-unmatchable': true,
+ 'selector-attribute-name-disallowed-list': null,
+ 'selector-attribute-operator-allowed-list': null,
+ 'selector-attribute-operator-disallowed-list': null,
+ 'selector-attribute-quotes': 'always',
+ 'selector-class-pattern': null,
+ 'selector-combinator-allowed-list': null,
+ 'selector-combinator-disallowed-list': null,
+ 'selector-disallowed-list': null,
+ 'selector-id-pattern': null,
+ 'selector-max-attribute': null,
+ 'selector-max-class': null,
+ 'selector-max-combinators': null,
+ 'selector-max-compound-selectors': null,
+ 'selector-max-id': null,
+ 'selector-max-pseudo-class': null,
+ 'selector-max-specificity': null,
+ 'selector-max-type': null,
+ 'selector-max-universal': null,
+ 'selector-nested-pattern': null,
+ 'selector-no-qualifying-type': null,
+ 'selector-no-vendor-prefix': true,
+ 'selector-not-notation': null,
+ 'selector-pseudo-class-allowed-list': null,
+ 'selector-pseudo-class-disallowed-list': null,
+ 'selector-pseudo-class-no-unknown': true,
+ 'selector-pseudo-element-allowed-list': null,
+ 'selector-pseudo-element-colon-notation': 'double',
+ 'selector-pseudo-element-disallowed-list': null,
+ 'selector-pseudo-element-no-unknown': true,
+ 'selector-type-case': 'lower',
+ 'selector-type-no-unknown': [true, {ignore: ['custom-elements']}],
+ 'shorthand-property-no-redundant-values': true,
+ 'string-no-newline': true,
+ 'time-min-milliseconds': null,
+ 'unit-allowed-list': null,
+ 'unit-disallowed-list': null,
+ 'unit-no-unknown': true,
+ 'value-keyword-case': null,
+ 'value-no-vendor-prefix': [true, {ignoreValues: ['box', 'inline-box']}],
+ },
+};
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000000..d49e9d7a1c
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,101 @@
+import {readFileSync} from 'node:fs';
+import {env} from 'node:process';
+import {parse} from 'postcss';
+
+const isProduction = env.NODE_ENV !== 'development';
+
+function extractRootVars(css) {
+ const root = parse(css);
+ const vars = new Set();
+ root.walkRules((rule) => {
+ if (rule.selector !== ':root') return;
+ rule.each((decl) => {
+ if (decl.value && decl.prop.startsWith('--')) {
+ vars.add(decl.prop.substring(2));
+ }
+ });
+ });
+ return Array.from(vars);
+}
+
+const vars = extractRootVars([
+ readFileSync(new URL('web_src/css/themes/theme-gitea-light.css', import.meta.url), 'utf8'),
+ readFileSync(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url), 'utf8'),
+].join('\n'));
+
+export default {
+ prefix: 'tw-',
+ important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles
+ content: [
+ isProduction && '!./templates/devtest/**/*',
+ isProduction && '!./web_src/js/standalone/devtest.js',
+ '!./templates/swagger/v1_json.tmpl',
+ '!./templates/user/auth/oidc_wellknown.tmpl',
+ '!**/*_test.go',
+ '!./modules/{public,options,templates}/bindata.go',
+ './{build,models,modules,routers,services}/**/*.go',
+ './templates/**/*.tmpl',
+ './web_src/js/**/*.{js,vue}',
+ ].filter(Boolean),
+ blocklist: [
+ // classes that don't work without CSS variables from "@tailwind base" which we don't use
+ 'transform', 'shadow', 'ring', 'blur', 'grayscale', 'invert', '!invert', 'filter', '!filter',
+ 'backdrop-filter',
+ // we use double-class tw-hidden defined in web_src/css/helpers.css for increased specificity
+ 'hidden',
+ // unneeded classes
+ '[-a-zA-Z:0-9_.]',
+ ],
+ theme: {
+ colors: {
+ // make `tw-bg-red` etc work with our CSS variables
+ ...Object.fromEntries(vars.filter((prop) => prop.startsWith('color-')).map((prop) => {
+ const color = prop.substring(6);
+ return [color, `var(--color-${color})`];
+ })),
+ inherit: 'inherit',
+ current: 'currentcolor',
+ transparent: 'transparent',
+ },
+ borderRadius: {
+ 'none': '0',
+ 'sm': '2px',
+ 'DEFAULT': 'var(--border-radius)', // 4px
+ 'md': 'var(--border-radius-medium)', // 6px
+ 'lg': '8px',
+ 'xl': '12px',
+ '2xl': '16px',
+ '3xl': '24px',
+ 'full': 'var(--border-radius-circle)', // 50%
+ },
+ fontFamily: {
+ sans: 'var(--fonts-regular)',
+ mono: 'var(--fonts-monospace)',
+ },
+ fontWeight: {
+ light: 'var(--font-weight-light)',
+ normal: 'var(--font-weight-normal)',
+ medium: 'var(--font-weight-medium)',
+ semibold: 'var(--font-weight-semibold)',
+ bold: 'var(--font-weight-bold)',
+ },
+ fontSize: { // not using `rem` units because our root is currently 14px
+ 'xs': '12px',
+ 'sm': '14px',
+ 'base': '16px',
+ 'lg': '18px',
+ 'xl': '20px',
+ '2xl': '24px',
+ '3xl': '30px',
+ '4xl': '36px',
+ '5xl': '48px',
+ '6xl': '60px',
+ '7xl': '72px',
+ '8xl': '96px',
+ '9xl': '128px',
+ ...Object.fromEntries(Array.from({length: 100}, (_, i) => {
+ return [`${i}`, `${i === 0 ? '0' : `${i}px`}`];
+ })),
+ },
+ },
+};
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 25abefae00..e140d6b5eb 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -42,7 +42,7 @@
-
+
@@ -113,7 +113,7 @@
-
+
@@ -148,7 +148,7 @@
-
+
@@ -205,7 +205,7 @@
{{ctx.Locale.Tr "admin.auths.force_smtps_helper"}}
-
+
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index f32f77d5dc..f130e18f65 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -33,13 +33,13 @@
{{template "admin/auth/source/smtp" .}}
-
+
-
+
@@ -59,7 +59,7 @@
-
+
@@ -99,7 +99,7 @@
- GitHub
{{ctx.Locale.Tr "admin.auths.tip.github"}}
- GitLab
- {{ctx.Locale.Tr "admin.auths.tip.gitlab"}}
+ {{ctx.Locale.Tr "admin.auths.tip.gitlab_new"}}
- Google
{{ctx.Locale.Tr "admin.auths.tip.google_plus"}}
- OpenID Connect
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index a584ac7628..9754aed55a 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -1,4 +1,4 @@
-
+
@@ -20,17 +20,17 @@
-
+
-
+
-
+
@@ -38,7 +38,7 @@
-
+
@@ -115,13 +115,13 @@
-
+
-
+
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl
index 63ad77e67b..f02c5bdf30 100644
--- a/templates/admin/auth/source/oauth.tmpl
+++ b/templates/admin/auth/source/oauth.tmpl
@@ -1,4 +1,4 @@
-
+
diff --git a/templates/admin/auth/source/smtp.tmpl b/templates/admin/auth/source/smtp.tmpl
index c4b0b0e7e4..31195acf65 100644
--- a/templates/admin/auth/source/smtp.tmpl
+++ b/templates/admin/auth/source/smtp.tmpl
@@ -1,4 +1,4 @@
-
+
diff --git a/templates/admin/auth/source/sspi.tmpl b/templates/admin/auth/source/sspi.tmpl
index f835e89bdf..6a3f00f9a8 100644
--- a/templates/admin/auth/source/sspi.tmpl
+++ b/templates/admin/auth/source/sspi.tmpl
@@ -1,4 +1,4 @@
-
+
diff --git a/templates/admin/base/search.tmpl b/templates/admin/base/search.tmpl
deleted file mode 100644
index 0fecb61d9e..0000000000
--- a/templates/admin/base/search.tmpl
+++ /dev/null
@@ -1,23 +0,0 @@
-- {{ctx.Locale.Tr "admin.config.mailer_user"}}
- {{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}
- - {{ctx.Locale.Tr "admin.config.send_test_mail"}}
+ - {{ctx.Locale.Tr "admin.config.send_test_mail"}}
-
-
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 1cc4b7bb09..8c16429920 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -229,7 +229,7 @@
- {{ctx.Locale.Tr "admin.config.picture_config"}}
-
-
-
- - {{ctx.Locale.Tr "admin.config.disable_gravatar"}}
- -
-
-
-
-
-
- - {{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}
- -
-
-
-
-
-
-
-
{{ctx.Locale.Tr "admin.config.git_config"}}
@@ -353,7 +332,7 @@
{{range $loggerName, $loggerDetail := .Loggers}}
- {{ctx.Locale.Tr "admin.config.logger_name_fmt" $loggerName}}
{{if $loggerDetail.IsEnabled}}
- {{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}
+ {{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}
{{else}}
- {{ctx.Locale.Tr "admin.config.disabled_logger"}}
{{end}}
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
new file mode 100644
index 0000000000..02ab5fd0fb
--- /dev/null
+++ b/templates/admin/config_settings.tmpl
@@ -0,0 +1,42 @@
+{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
+
+ {{ctx.Locale.Tr "admin.config.picture_config"}}
+
+
+
+ - {{ctx.Locale.Tr "admin.config.disable_gravatar"}}
+ -
+
+
+
+
+
+ - {{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}
+ -
+
+
+
+
+
+
+
+
+ {{ctx.Locale.Tr "repository"}}
+
+
+
+
+{{template "admin/layout_footer" .}}
diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl
index af30cc06e1..3cb641488c 100644
--- a/templates/admin/cron.tmpl
+++ b/templates/admin/cron.tmpl
@@ -5,7 +5,7 @@