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) <i@wolfogre.com> - [Lunny Xiao](https://gitea.com/lunny) <xiaolunwen@gmail.com> - - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.io> + - [Matti Ranta](https://gitea.com/techknowlogick) <techknowlogick@gitea.com> - Community - [6543](https://gitea.com/6543) <6543@obermui.de> - - [Andrew Thornton](https://gitea.com/zeripath) <art27@cantab.net> + - [delvh](https://gitea.com/delvh) <dev.lh@web.de> - [John Olheiser](https://gitea.com/jolheiser) <john.olheiser@gmail.com> ### Previous TOC/owners members Here's the history of the owners and the time they served: -- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872) +- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 - [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017 - [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017 - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801) -- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872) -- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872) +- [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [6543](https://gitea.com/6543) - 2023 +- [John Olheiser](https://gitea.com/jolheiser) - 2023 +- [Jason Song](https://gitea.com/wolfogre) - 2023 ## Governance Compensation diff --git a/Dockerfile b/Dockerfile index 325b0255df..b647c0cd59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.21-alpine3.19 AS build-env +FROM docker.io/library/golang:1.22-alpine3.19 AS build-env ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 6f27c698ac..dd7da97278 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.21-alpine3.19 AS build-env +FROM docker.io/library/golang:1.22-alpine3.19 AS build-env ARG GOPROXY ENV GOPROXY ${GOPROXY:-direct} diff --git a/MAINTAINERS b/MAINTAINERS index 72171f80ed..eed87529a3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -59,3 +59,5 @@ Rui Chen <rui@chenrui.dev> (@chenrui333) Nanguan Lin <nanguanlin6@gmail.com> (@lng2020) kerwin612 <kerwin612@qq.com> (@kerwin612) Gary Wang <git@blumia.net> (@BLumia) +Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis) +Yu Liu <1240335630@qq.com> (@HEREYUA) diff --git a/Makefile b/Makefile index 273ae1fa68..f1acfbc81e 100644 --- a/Makefile +++ b/Makefile @@ -23,28 +23,25 @@ SHASUM ?= shasum -a 256 HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) COMMA := , -XGO_VERSION := go-1.21.x +XGO_VERSION := go-1.22.x -AIR_PACKAGE ?= github.com/cosmtrek/air@v1.49.0 +AIR_PACKAGE ?= github.com/cosmtrek/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 -MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest -GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 -GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3 -ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.6.26 +GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 +GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 +ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) ifeq ($(HAS_GO), yes) - GOPATH ?= $(shell $(GO) env GOPATH) - export PATH := $(GOPATH)/bin:$(PATH) - CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766 CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) endif @@ -115,13 +112,14 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) +MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) FOMANTIC_WORK_DIR := web_src/fomantic WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) -WEBPACK_CONFIGS := webpack.config.js +WEBPACK_CONFIGS := webpack.config.js tailwind.config.js WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css -WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/img/webpack +WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) @@ -146,6 +144,11 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN GO_DIRS := build cmd models modules routers services tests WEB_DIRS := web_src/js web_src/css +ESLINT_FILES := web_src/js tools *.config.js tests/e2e +STYLELINT_FILES := web_src/css web_src/js/components/*.vue +SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github +EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini + GO_SOURCES := $(wildcard *.go) GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go) GO_SOURCES += $(GENERATED_GO_DEST) @@ -162,8 +165,8 @@ ifdef DEPS_PLAYWRIGHT endif SWAGGER_SPEC := templates/swagger/v1_json.tmpl -SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g -SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g +SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g +SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g SWAGGER_EXCLUDE := code.gitea.io/sdk SWAGGER_NEWLINE_COMMAND := -e '$$a\' @@ -219,6 +222,8 @@ help: @echo " - lint-swagger lint swagger files" @echo " - lint-templates lint template files" @echo " - lint-yaml lint yaml files" + @echo " - lint-spell lint spelling" + @echo " - lint-spell-fix lint spelling and fix issues" @echo " - checks run various consistency checks" @echo " - checks-frontend check frontend files" @echo " - checks-backend check backend files" @@ -308,10 +313,6 @@ fmt-check: fmt exit 1; \ fi -.PHONY: misspell-check -misspell-check: - go run $(MISSPELL_PACKAGE) -error $(GO_DIRS) $(WEB_DIRS) - .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @mkdir -p $(MAKE_EVIDENCE_DIR) @@ -351,13 +352,13 @@ checks: checks-frontend checks-backend checks-frontend: lockfile-check svg-check .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate security-check +checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check .PHONY: lint -lint: lint-frontend lint-backend +lint: lint-frontend lint-backend lint-spell .PHONY: lint-fix -lint-fix: lint-frontend-fix lint-backend-fix +lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix .PHONY: lint-frontend lint-frontend: lint-js lint-css @@ -373,19 +374,19 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig .PHONY: lint-js lint-js: node_modules - npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e + npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) .PHONY: lint-js-fix lint-js-fix: node_modules - npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e --fix + npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) --fix .PHONY: lint-css lint-css: node_modules - npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue + npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) .PHONY: lint-css-fix lint-css-fix: node_modules - npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue --fix + npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix .PHONY: lint-swagger lint-swagger: node_modules @@ -395,6 +396,14 @@ lint-swagger: node_modules lint-md: node_modules npx markdownlint docs *.md +.PHONY: lint-spell +lint-spell: + @go run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES) + +.PHONY: lint-spell-fix +lint-spell-fix: + @go run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES) + .PHONY: lint-go lint-go: $(GO) run $(GOLANGCI_LINT_PACKAGE) run @@ -418,14 +427,15 @@ lint-go-vet: .PHONY: lint-editorconfig lint-editorconfig: - $(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .github/workflows + @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) .PHONY: lint-actions lint-actions: $(GO) run $(ACTIONLINT_PACKAGE) .PHONY: lint-templates -lint-templates: .venv +lint-templates: .venv node_modules + @node tools/lint-templates-svg.js @poetry run djlint $(shell find templates -type f -iname '*.tmpl') .PHONY: lint-yaml @@ -434,7 +444,7 @@ lint-yaml: .venv .PHONY: watch watch: - @bash build/watch.sh + @bash tools/watch.sh .PHONY: watch-frontend watch-frontend: node-check node_modules @@ -594,8 +604,7 @@ test-mssql\#%: integrations.mssql.test generate-ini-mssql test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test .PHONY: playwright -playwright: $(PLAYWRIGHT_DIR) - npm install --no-save @playwright/test +playwright: deps-frontend npx playwright install $(PLAYWRIGHT_FLAGS) .PHONY: test-e2e% @@ -702,9 +711,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite .PHONY: migrations.individual.mysql.test migrations.individual.mysql.test: $(GO_SOURCES) - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \ - done + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.sqlite.test\#% migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite @@ -712,20 +719,15 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite .PHONY: migrations.individual.pgsql.test migrations.individual.pgsql.test: $(GO_SOURCES) - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \ - done + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.pgsql.test\#% migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* - .PHONY: migrations.individual.mssql.test migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg -test.failfast; \ - done + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.mssql.test\#% migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql @@ -733,9 +735,7 @@ migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql .PHONY: migrations.individual.sqlite.test migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \ - done + GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.sqlite.test\#% migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite @@ -839,10 +839,6 @@ release-sources: | $(DIST_DIRS) release-docs: | $(DIST_DIRS) docs tar -czf $(DIST)/release/gitea-docs-$(VERSION).tar.gz -C ./docs . -.PHONY: docs -docs: - cd docs; bash scripts/trans-copy.sh; - .PHONY: deps deps: deps-frontend deps-backend deps-tools deps-py @@ -901,6 +897,7 @@ fomantic: cd $(FOMANTIC_WORK_DIR) && npm install --no-save cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/ + $(SED_INPLACE) -e 's/ overrideBrowserslist\r/ overrideBrowserslist: ["defaults"]\r/g' $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/tasks/config/tasks.js cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build # fomantic uses "touchstart" as click event for some browsers, it's not ideal, so we force fomantic to always use "click" as click event $(SED_INPLACE) -e 's/clickEvent[ \t]*=/clickEvent = "click", unstableClickEvent =/g' $(FOMANTIC_WORK_DIR)/build/semantic.js @@ -919,7 +916,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json .PHONY: svg svg: node-check | node_modules rm -rf $(SVG_DEST_DIR) - node build/generate-svg.js + node tools/generate-svg.js .PHONY: svg-check svg-check: svg @@ -962,8 +959,8 @@ generate-gitignore: .PHONY: generate-images generate-images: | node_modules - npm install --no-save --no-package-lock fabric@5 imagemin-zopfli@7 - node build/generate-images.js $(TAGS) + npm install --no-save fabric@6.0.0-beta20 imagemin-zopfli@7 + node tools/generate-images.js $(TAGS) .PHONY: generate-manpage generate-manpage: @@ -980,3 +977,8 @@ docker: # This endif closes the if at the top of the file endif + +# Disable parallel execution because it would break some targets that don't +# specify exact dependencies like 'backend' which does currently not depend +# on 'frontend' to enable Node.js-less builds from source tarballs. +.NOTPARALLEL: diff --git a/README.md b/README.md index 5356fcfacd..f579449174 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,18 @@ -<p align="center"> - <a href="https://gitea.io/"> - <img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/> - </a> -</p> -<h1 align="center">Gitea - Git with a cup of tea</h1> +# Gitea -<p align="center"> - <a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly"> - <img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main"> - </a> - <a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea"> - <img src="https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2"> - </a> - <a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov"> - <img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg"> - </a> - <a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card"> - <img src="https://goreportcard.com/badge/code.gitea.io/gitea"> - </a> - <a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc"> - <img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg"> - </a> - <a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release"> - <img src="https://img.shields.io/github/release/go-gitea/gitea.svg"> - </a> - <a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source"> - <img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg"> - </a> - <a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea"> - <img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen"> - </a> - <a href="https://opensource.org/licenses/MIT" title="License: MIT"> - <img src="https://img.shields.io/badge/License-MIT-blue.svg"> - </a> - <a href="https://gitpod.io/#https://github.com/go-gitea/gitea"> - <img - src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod" - alt="Contribute with Gitpod" - /> - </a> - <a href="https://crowdin.com/project/gitea" title="Crowdin"> - <img src="https://badges.crowdin.net/gitea/localized.svg"> - </a> - <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs"> - <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main"> - </a> - <a href="https://app.bountysource.com/teams/gitea" title="Bountysource"> - <img src="https://img.shields.io/bountysource/team/gitea/activity"> - </a> -</p> +[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") +[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") +[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") +[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") +[](https://github.com/go-gitea/gitea/releases/latest "GitHub release") +[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") +[](https://opencollective.com/gitea "Become a backer/sponsor of gitea") +[](https://opensource.org/licenses/MIT "License: MIT") +[](https://gitpod.io/#https://github.com/go-gitea/gitea) +[](https://crowdin.com/project/gitea "Crowdin") +[](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs") -<p align="center"> - <a href="README_ZH.md">View this document in Chinese</a> -</p> +[View this document in Chinese](./README_ZH.md) ## Purpose @@ -89,25 +49,23 @@ The `build` target is split into two sub-targets: Internet connectivity is required to download the go and npm modules. When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js. -Parallelism (`make -j <num>`) is not supported. - More info: https://docs.gitea.com/installation/install-from-source ## Using ./gitea web -NOTE: If you're interested in using our APIs, we have experimental -support with [documentation](https://try.gitea.io/api/swagger). +> [!NOTE] +> If you're interested in using our APIs, we have experimental support with [documentation](https://try.gitea.io/api/swagger). ## Contributing Expected workflow is: Fork -> Patch -> Push -> Pull Request -NOTES: - -1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.** -2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks! +> [!NOTE] +> +> 1. **YOU MUST READ THE [CONTRIBUTORS GUIDE](CONTRIBUTING.md) BEFORE STARTING TO WORK ON A PULL REQUEST.** +> 2. If you have found a vulnerability in the project, please write privately to **security@gitea.io**. Thanks! ## Translating diff --git a/README_ZH.md b/README_ZH.md index 0d9092a0fd..726c4273a6 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -1,58 +1,18 @@ -<p align="center"> - <a href="https://gitea.io/"> - <img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/> - </a> -</p> -<h1 align="center">Gitea - Git with a cup of tea</h1> +# Gitea -<p align="center"> - <a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly"> - <img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main"> - </a> - <a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea"> - <img src="https://img.shields.io/discord/322538954119184384.svg"> - </a> - <a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov"> - <img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg"> - </a> - <a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card"> - <img src="https://goreportcard.com/badge/code.gitea.io/gitea"> - </a> - <a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc"> - <img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg"> - </a> - <a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release"> - <img src="https://img.shields.io/github/release/go-gitea/gitea.svg"> - </a> - <a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source"> - <img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg"> - </a> - <a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea"> - <img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen"> - </a> - <a href="https://opensource.org/licenses/MIT" title="License: MIT"> - <img src="https://img.shields.io/badge/License-MIT-blue.svg"> - </a> - <a href="https://gitpod.io/#https://github.com/go-gitea/gitea"> - <img - src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod" - alt="Contribute with Gitpod" - /> - </a> - <a href="https://crowdin.com/project/gitea" title="Crowdin"> - <img src="https://badges.crowdin.net/gitea/localized.svg"> - </a> - <a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs"> - <img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main"> - </a> - <a href="https://app.bountysource.com/teams/gitea" title="Bountysource"> - <img src="https://img.shields.io/bountysource/team/gitea/activity"> - </a> -</p> +[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") +[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") +[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") +[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") +[](https://github.com/go-gitea/gitea/releases/latest "GitHub release") +[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") +[](https://opencollective.com/gitea "Become a backer/sponsor of gitea") +[](https://opensource.org/licenses/MIT "License: MIT") +[](https://gitpod.io/#https://github.com/go-gitea/gitea) +[](https://crowdin.com/project/gitea "Crowdin") +[](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs") -<p align="center"> - <a href="README.md">View this document in English</a> -</p> +[View this document in English](./README.md) ## 目标 diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 2aa60780c4..ea73182a83 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -24,11 +24,21 @@ "path": "codeberg.org/gusted/mcaptcha/LICENSE", "licenseText": "Copyright © 2022 William Zijl\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, + { + "name": "connectrpc.com/connect", + "path": "connectrpc.com/connect/LICENSE", + "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2021-2024 The Connect Authors\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" + }, { "name": "dario.cat/mergo", "path": "dario.cat/mergo/LICENSE", "licenseText": "Copyright (c) 2013 Dario Castañé. All rights reserved.\nCopyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "filippo.io/edwards25519", + "path": "filippo.io/edwards25519/LICENSE", + "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "git.sr.ht/~mariusor/go-xsd-duration", "path": "git.sr.ht/~mariusor/go-xsd-duration/LICENSE", @@ -234,11 +244,6 @@ "path": "github.com/bradfitz/gomemcache/memcache/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, - { - "name": "github.com/bufbuild/connect-go", - "path": "github.com/bufbuild/connect-go/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2021-2022 Buf Technologies, Inc.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/buildkite/terminal-to-html/v3", "path": "github.com/buildkite/terminal-to-html/v3/LICENSE", @@ -299,11 +304,6 @@ "path": "github.com/davecgh/go-spew/spew/LICENSE", "licenseText": "ISC License\n\nCopyright (c) 2012-2016 Dave Collins \u003cdave@davec.name\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n" }, - { - "name": "github.com/denisenkom/go-mssqldb", - "path": "github.com/denisenkom/go-mssqldb/LICENSE.txt", - "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "github.com/dgryski/go-rendezvous", "path": "github.com/dgryski/go-rendezvous/LICENSE", @@ -535,8 +535,8 @@ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/golang/protobuf", - "path": "github.com/golang/protobuf/LICENSE", + "name": "github.com/golang/protobuf/proto", + "path": "github.com/golang/protobuf/proto/LICENSE", "licenseText": "Copyright 2010 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n" }, { @@ -754,6 +754,16 @@ "path": "github.com/microcosm-cc/bluemonday/LICENSE.md", "licenseText": "SPDX short identifier: BSD-3-Clause\nhttps://opensource.org/licenses/BSD-3-Clause\n\nCopyright (c) 2014, David Kitchen \u003cdavid@buro9.com\u003e\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the organisation (Microcosm) nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, + { + "name": "github.com/microsoft/go-mssqldb", + "path": "github.com/microsoft/go-mssqldb/LICENSE.txt", + "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) Microsoft Corporation.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg", + "path": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg/LICENSE.txt", + "licenseText": "Copyright (c) 2021 Swisscom (Switzerland) Ltd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\n" + }, { "name": "github.com/miekg/dns", "path": "github.com/miekg/dns/LICENSE", @@ -1066,7 +1076,7 @@ }, { "name": "go.uber.org/zap", - "path": "go.uber.org/zap/LICENSE.txt", + "path": "go.uber.org/zap/LICENSE", "licenseText": "Copyright (c) 2016-2017 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, { diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index 0db505ff9c..ab769f6d0c 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -4,8 +4,8 @@ package cmd import ( - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/modules/graceful" + asymkey_service "code.gitea.io/gitea/services/asymkey" repo_service "code.gitea.io/gitea/services/repository" "github.com/urfave/cli/v2" @@ -42,5 +42,5 @@ func runRegenerateKeys(_ *cli.Context) error { if err := initDB(ctx); err != nil { return err } - return asymkey_model.RewriteAllPublicKeys(ctx) + return asymkey_service.RewriteAllPublicKeys(ctx) } diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index fefe18d39c..a257ce21c8 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -10,8 +10,8 @@ import ( auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" pwd "code.gitea.io/gitea/modules/auth/password" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/urfave/cli/v2" ) @@ -123,10 +123,10 @@ func runCreateUser(c *cli.Context) error { changePassword = c.Bool("must-change-password") } - restricted := util.OptionalBoolNone + restricted := optional.None[bool]() if c.IsSet("restricted") { - restricted = util.OptionalBoolOf(c.Bool("restricted")) + restricted = optional.Some(c.Bool("restricted")) } // default user visibility in app.ini @@ -142,7 +142,7 @@ func runCreateUser(c *cli.Context) error { } overwriteDefault := &user_model.CreateUserOverwriteOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), IsRestricted: restricted, } diff --git a/cmd/dump.go b/cmd/dump.go index 69ecdcec12..da0a51d9ce 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -6,14 +6,13 @@ package cmd import ( "fmt" - "io" "os" "path" "path/filepath" "strings" - "time" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/dump" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -25,89 +24,17 @@ import ( "github.com/urfave/cli/v2" ) -func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error { - if verbose { - log.Info("Adding file %s", customName) - } - - return w.Write(archiver.File{ - FileInfo: archiver.FileInfo{ - FileInfo: info, - CustomName: customName, - }, - ReadCloser: r, - }) -} - -func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error { - file, err := os.Open(absPath) - if err != nil { - return err - } - defer file.Close() - fileInfo, err := file.Stat() - if err != nil { - return err - } - - return addReader(w, file, fileInfo, filePath, verbose) -} - -func isSubdir(upper, lower string) (bool, error) { - if relPath, err := filepath.Rel(upper, lower); err != nil { - return false, err - } else if relPath == "." || !strings.HasPrefix(relPath, ".") { - return true, nil - } - return false, nil -} - -type outputType struct { - Enum []string - Default string - selected string -} - -func (o outputType) Join() string { - return strings.Join(o.Enum, ", ") -} - -func (o *outputType) Set(value string) error { - for _, enum := range o.Enum { - if enum == value { - o.selected = value - return nil - } - } - - return fmt.Errorf("allowed values are %s", o.Join()) -} - -func (o outputType) String() string { - if o.selected == "" { - return o.Default - } - return o.selected -} - -var outputTypeEnum = &outputType{ - Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}, - Default: "zip", -} - // CmdDump represents the available dump sub-command. var CmdDump = &cli.Command{ - Name: "dump", - Usage: "Dump Gitea files and database", - Description: `Dump compresses all related files and database into zip file. -It can be used for backup and capture Gitea server image to send to maintainer`, - Action: runDump, + Name: "dump", + Usage: "Dump Gitea files and database", + Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, + Action: runDump, Flags: []cli.Flag{ &cli.StringFlag{ Name: "file", Aliases: []string{"f"}, - Value: fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()), - Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.", + Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, }, &cli.BoolFlag{ Name: "verbose", @@ -160,64 +87,52 @@ It can be used for backup and capture Gitea server image to send to maintainer`, Name: "skip-index", Usage: "Skip bleve index data", }, - &cli.GenericFlag{ + &cli.StringFlag{ Name: "type", - Value: outputTypeEnum, - Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()), + Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")), }, }, } func fatal(format string, args ...any) { - fmt.Fprintf(os.Stderr, format+"\n", args...) log.Fatal(format, args...) } func runDump(ctx *cli.Context) error { - var file *os.File - fileName := ctx.String("file") - outType := ctx.String("type") - if fileName == "-" { - file = os.Stdout - setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) - } else { - for _, suffix := range outputTypeEnum.Enum { - if strings.HasSuffix(fileName, "."+suffix) { - fileName = strings.TrimSuffix(fileName, "."+suffix) - break - } - } - fileName += "." + outType - } setting.MustInstalled() - // make sure we are logging to the console no matter what the configuration tells us do to - // FIXME: don't use CfgProvider directly - if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil { - fatal("Setting logging mode to console failed: %v", err) - } - if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil { - fatal("Setting console logger to stderr failed: %v", err) - } - - // Set loglevel to Warn if quiet-mode is requested - if ctx.Bool("quiet") { - if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil { - fatal("Setting console log-level failed: %v", err) - } - } - - if !setting.InstallLock { - log.Error("Is '%s' really the right config path?\n", setting.CustomConf) - return fmt.Errorf("gitea is not initialized") - } - setting.LoadSettings() // cannot access session settings otherwise - + quite := ctx.Bool("quiet") verbose := ctx.Bool("verbose") - if verbose && ctx.Bool("quiet") { - return fmt.Errorf("--quiet and --verbose cannot both be set") + if verbose && quite { + fatal("Option --quiet and --verbose cannot both be set") } + // outFileName is either "-" or a file name (will be made absolute) + outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type")) + if outType == "" { + fatal("Invalid output type") + } + + outFile := os.Stdout + if outFileName != "-" { + var err error + if outFileName, err = filepath.Abs(outFileName); err != nil { + fatal("Unable to get absolute path of dump file: %v", err) + } + if exist, _ := util.IsExist(outFileName); exist { + fatal("Dump file %q exists", outFileName) + } + if outFile, err = os.Create(outFileName); err != nil { + fatal("Unable to create dump file %q: %v", outFileName, err) + } + defer outFile.Close() + } + + setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr) + + setting.DisableLoggerInit() + setting.LoadSettings() // cannot access session settings otherwise + stdCtx, cancel := installSignals() defer cancel() @@ -226,44 +141,32 @@ func runDump(ctx *cli.Context) error { return err } - if err := storage.Init(); err != nil { + if err = storage.Init(); err != nil { return err } - if file == nil { - file, err = os.Create(fileName) - if err != nil { - fatal("Unable to open %s: %v", fileName, err) - } - } - defer file.Close() - - absFileName, err := filepath.Abs(fileName) - if err != nil { - return err - } - - var iface any - if fileName == "-" { - iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType)) - } else { - iface, err = archiver.ByExtension(fileName) - } + archiverGeneric, err := archiver.ByExtension("." + outType) if err != nil { fatal("Unable to get archiver for extension: %v", err) } - w, _ := iface.(archiver.Writer) - if err := w.Create(file); err != nil { + archiverWriter := archiverGeneric.(archiver.Writer) + if err := archiverWriter.Create(outFile); err != nil { fatal("Creating archiver.Writer failed: %v", err) } - defer w.Close() + defer archiverWriter.Close() + + dumper := &dump.Dumper{ + Writer: archiverWriter, + Verbose: verbose, + } + dumper.GlobalExcludeAbsPath(outFileName) if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") { log.Info("Skip dumping local repositories") } else { log.Info("Dumping local repositories... %s", setting.RepoRootPath) - if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil { + if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil { fatal("Failed to include repositories: %v", err) } @@ -276,8 +179,7 @@ func runDump(ctx *cli.Context) error { if err != nil { return err } - - return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose) + return dumper.AddReader(object, info, path.Join("data", "lfs", objPath)) }); err != nil { fatal("Failed to dump LFS objects: %v", err) } @@ -310,15 +212,13 @@ func runDump(ctx *cli.Context) error { fatal("Failed to dump database: %v", err) } - if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil { + if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil { fatal("Failed to include gitea-db.sql: %v", err) } - if len(setting.CustomConf) > 0 { - log.Info("Adding custom configuration file from %s", setting.CustomConf) - if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil { - fatal("Failed to include specified app.ini: %v", err) - } + log.Info("Adding custom configuration file from %s", setting.CustomConf) + if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil { + fatal("Failed to include specified app.ini: %v", err) } if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { @@ -326,8 +226,8 @@ func runDump(ctx *cli.Context) error { } else { customDir, err := os.Stat(setting.CustomPath) if err == nil && customDir.IsDir() { - if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is { - if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil { + if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is { + if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil { fatal("Failed to include custom: %v", err) } } else { @@ -364,8 +264,7 @@ func runDump(ctx *cli.Context) error { excludes = append(excludes, setting.Attachment.Storage.Path) excludes = append(excludes, setting.Packages.Storage.Path) excludes = append(excludes, setting.Log.RootPath) - excludes = append(excludes, absFileName) - if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil { + if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil { fatal("Failed to include data directory: %v", err) } } @@ -377,8 +276,7 @@ func runDump(ctx *cli.Context) error { if err != nil { return err } - - return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose) + return dumper.AddReader(object, info, path.Join("data", "attachments", objPath)) }); err != nil { fatal("Failed to dump attachments: %v", err) } @@ -392,8 +290,7 @@ func runDump(ctx *cli.Context) error { if err != nil { return err } - - return addReader(w, object, info, path.Join("data", "packages", objPath), verbose) + return dumper.AddReader(object, info, path.Join("data", "packages", objPath)) }); err != nil { fatal("Failed to dump packages: %v", err) } @@ -409,80 +306,23 @@ func runDump(ctx *cli.Context) error { log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err) } if isExist { - if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil { + if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil { fatal("Failed to include log: %v", err) } } } - if fileName != "-" { - if err = w.Close(); err != nil { - _ = util.Remove(fileName) - fatal("Failed to save %s: %v", fileName, err) + if outFileName == "-" { + log.Info("Finish dumping to stdout") + } else { + if err = archiverWriter.Close(); err != nil { + _ = os.Remove(outFileName) + fatal("Failed to save %q: %v", outFileName, err) } - - if err := os.Chmod(fileName, 0o600); err != nil { + if err = os.Chmod(outFileName, 0o600); err != nil { log.Info("Can't change file access permissions mask to 0600: %v", err) } - } - - if fileName != "-" { - log.Info("Finish dumping in file %s", fileName) - } else { - log.Info("Finish dumping to stdout") - } - - return nil -} - -// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath -func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error { - absPath, err := filepath.Abs(absPath) - if err != nil { - return err - } - dir, err := os.Open(absPath) - if err != nil { - return err - } - defer dir.Close() - - files, err := dir.Readdir(0) - if err != nil { - return err - } - for _, file := range files { - currentAbsPath := filepath.Join(absPath, file.Name()) - currentInsidePath := path.Join(insidePath, file.Name()) - if file.IsDir() { - if !util.SliceContainsString(excludeAbsPath, currentAbsPath) { - if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil { - return err - } - if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil { - return err - } - } - } else { - // only copy regular files and symlink regular files, skip non-regular files like socket/pipe/... - shouldAdd := file.Mode().IsRegular() - if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink { - target, err := filepath.EvalSymlinks(currentAbsPath) - if err != nil { - return err - } - targetStat, err := os.Stat(target) - if err != nil { - return err - } - shouldAdd = targetStat.Mode().IsRegular() - } - if shouldAdd { - if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil { - return err - } - } - } + log.Info("Finish dumping in file %s", outFileName) } return nil } diff --git a/cmd/generate.go b/cmd/generate.go index 4ab10da22a..90b32ecaf0 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -70,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error { } func runGenerateLfsJwtSecret(c *cli.Context) error { - _, jwtSecretBase64, err := generate.NewJwtSecretBase64() + _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() if err != nil { return err } diff --git a/cmd/hook.go b/cmd/hook.go index 6a3358853d..c04591d79e 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -448,23 +448,26 @@ Gitea or set your environment appropriately.`, "") func hookPrintResults(results []private.HookPostReceiveBranchResult) { for _, res := range results { - if !res.Message { - continue - } - - fmt.Fprintln(os.Stderr, "") - if res.Create { - fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch) - fmt.Fprintf(os.Stderr, " %s\n", res.URL) - } else { - fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") - fmt.Fprintf(os.Stderr, " %s\n", res.URL) - } - fmt.Fprintln(os.Stderr, "") - os.Stderr.Sync() + hookPrintResult(res.Message, res.Create, res.Branch, res.URL) } } +func hookPrintResult(output, isCreate bool, branch, url string) { + if !output { + return + } + fmt.Fprintln(os.Stderr, "") + if isCreate { + fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch) + fmt.Fprintf(os.Stderr, " %s\n", url) + } else { + fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") + fmt.Fprintf(os.Stderr, " %s\n", url) + } + fmt.Fprintln(os.Stderr, "") + os.Stderr.Sync() +} + func pushOptions() map[string]string { opts := make(map[string]string) if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil { @@ -691,6 +694,12 @@ Gitea or set your environment appropriately.`, "") } err = writeFlushPktLine(ctx, os.Stdout) + if err == nil { + for _, res := range resp.Results { + hookPrintResult(res.ShouldShowMessage, res.IsCreatePR, res.HeadBranch, res.URL) + } + } + return err } diff --git a/cmd/keys.go b/cmd/keys.go index ceeec48486..7fdbe16119 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -71,7 +71,7 @@ func runKeys(c *cli.Context) error { ctx, cancel := installSignals() defer cancel() - setup(ctx, false) + setup(ctx, c.Bool("debug")) authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content) // do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys diff --git a/cmd/serv.go b/cmd/serv.go index 726663660b..90190a19db 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -63,21 +63,10 @@ func setup(ctx context.Context, debug bool) { setupConsoleLogger(log.FATAL, false, os.Stderr) } setting.MustInstalled() - if debug { - setting.RunMode = "dev" - } - - // Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when - // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection. if _, err := os.Stat(setting.RepoRootPath); err != nil { - if os.IsNotExist(err) { - _ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) - } else { - _ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) - } + _ = fail(ctx, "Unable to access repository path", "Unable to access repository path %q, err: %v", setting.RepoRootPath, err) return } - if err := git.InitSimple(context.Background()); err != nil { _ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err) } @@ -216,16 +205,18 @@ func runServ(c *cli.Context) error { } } - // LowerCase and trim the repoPath as that's how they are stored. - repoPath = strings.ToLower(strings.TrimSpace(repoPath)) - rr := strings.SplitN(repoPath, "/", 2) if len(rr) != 2 { return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) } - username := strings.ToLower(rr[0]) - reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git")) + username := rr[0] + reponame := strings.TrimSuffix(rr[1], ".git") + + // LowerCase and trim the repoPath as that's how they are stored. + // This should be done after splitting the repoPath into username and reponame + // so that username and reponame are not affected. + repoPath = strings.ToLower(strings.TrimSpace(repoPath)) if alphaDashDotPattern.MatchString(reponame) { return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) diff --git a/cmd/web.go b/cmd/web.go index 01386251be..ef8a7426c1 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -114,7 +114,7 @@ func showWebStartupMessage(msg string) { log.Info("* WorkPath: %s", setting.AppWorkPath) log.Info("* CustomPath: %s", setting.CustomPath) log.Info("* ConfigFile: %s", setting.CustomConf) - log.Info("%s", msg) + log.Info("%s", msg) // show startup message } func serveInstall(ctx *cli.Context) error { diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service index d205c6ee8b..c091722a74 100644 --- a/contrib/systemd/gitea.service +++ b/contrib/systemd/gitea.service @@ -1,6 +1,5 @@ [Unit] Description=Gitea (Git with a cup of tea) -After=syslog.target After=network.target ### # Don't forget to add the database service dependencies diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000000..35a38d768c --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,12 @@ +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_KEY +base_path: "." +base_url: "https://api.crowdin.com" +preserve_hierarchy: true +files: + - source: "/options/locale/locale_en-US.ini" + translation: "/options/locale/locale_%locale%.ini" + type: "ini" + skip_untranslated_strings: true + export_only_approved: true + update_option: "update_as_unapproved" diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 363bbcb151..918252044b 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -412,6 +412,10 @@ USER = root ;; ;; Whether execute database models migrations automatically ;AUTO_MIGRATION = true +;; +;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger +;; +;SLOW_QUERY_THRESHOLD = 5s ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -437,7 +441,7 @@ INTERNAL_TOKEN = ;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token ;; ;; How long to remember that a user is logged in before requiring relogin (in days) -;LOGIN_REMEMBER_DAYS = 7 +;LOGIN_REMEMBER_DAYS = 31 ;; ;; Name of the cookie used to store the current username. ;COOKIE_USERNAME = gitea_awesome @@ -952,6 +956,12 @@ LEVEL = Info ;GO_GET_CLONE_URL_PROTOCOL = https ;; ;; Close issues as long as a commit on any branch marks it as fixed +;DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false +;; +;; Allow users to push local repositories to Gitea and have them automatically created for a user or an org +;ENABLE_PUSH_CREATE_USER = false +;ENABLE_PUSH_CREATE_ORG = false +;; ;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects, repo.packages, repo.actions. ;DISABLED_REPO_UNITS = ;; @@ -1044,7 +1054,7 @@ LEVEL = Info ;; List of keywords used in Pull Request comments to automatically reopen a related issue ;REOPEN_KEYWORDS = reopen,reopens,reopened ;; -;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash +;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash, fast-forward-only ;DEFAULT_MERGE_STYLE = merge ;; ;; In the default merge message for squash commits include at most this many commits @@ -1470,6 +1480,16 @@ LEVEL = Info ;; ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled +;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future +;; - deletion: a user cannot delete their own account +;; - manage_ssh_keys: a user cannot configure ssh keys +;; - manage_gpg_keys: a user cannot configure gpg keys +;USER_DISABLED_FEATURES = +;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. +;; - deletion: a user cannot delete their own account +;; - manage_ssh_keys: a user cannot configure ssh keys +;; - manage_gpg_keys: a user cannot configure gpg keys +;;EXTERNAL_USER_DISABLE_FEATURES = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2295,6 +2315,8 @@ LEVEL = Info ;SHOW_FOOTER_VERSION = true ;; Show template execution time in the footer ;SHOW_FOOTER_TEMPLATE_LOAD_TIME = true +;; Show the "powered by" text in the footer +;SHOW_FOOTER_POWERED_BY = true ;; Generate sitemap. Defaults to `true`. ;ENABLE_SITEMAP = true ;; Enable/Disable RSS/Atom feed @@ -2593,7 +2615,7 @@ LEVEL = Info ;ENDLESS_TASK_TIMEOUT = 3h ;; Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time ;ABANDONED_JOB_TIMEOUT = 24h -;; Strings committers can place inside a commit message to skip executing the corresponding actions workflow +;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow ;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/backup-and-restore.en-us.md b/docs/content/administration/backup-and-restore.en-us.md index d46efecf99..451ef5c944 100644 --- a/docs/content/administration/backup-and-restore.en-us.md +++ b/docs/content/administration/backup-and-restore.en-us.md @@ -92,7 +92,7 @@ cd gitea-dump-1610949662 mv app.ini /etc/gitea/conf/app.ini mv data/* /var/lib/gitea/data/ mv log/* /var/lib/gitea/log/ -mv repos/* /var/lib/gitea/gitea-repositories/ +mv repos/* /var/lib/gitea/data/gitea-repositories/ chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea # mysql @@ -111,6 +111,8 @@ With Gitea running, and from the directory Gitea's binary is located, execute: ` This ensures that application and configuration file paths in repository Git Hooks are consistent and applicable to the current installation. If these paths are not updated, repository `push` actions will fail. +If you still have issues, consider running `./gitea doctor check` to inspect possible errors (or run with `--fix`). + ### Using Docker (`restore`) There is also no support for a recovery command in a Docker-based gitea instance. The restore process contains the same steps as described in the previous section but with different paths. diff --git a/docs/content/administration/backup-and-restore.zh-cn.md b/docs/content/administration/backup-and-restore.zh-cn.md index 98d378d5dc..db7eba84f7 100644 --- a/docs/content/administration/backup-and-restore.zh-cn.md +++ b/docs/content/administration/backup-and-restore.zh-cn.md @@ -19,6 +19,12 @@ menu: Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一个zip压缩文件。该压缩文件可以被用来进行数据恢复。 +## 备份一致性 + +为了确保 Gitea 实例的一致性,在备份期间必须关闭它。 + +Gitea 包括数据库、文件和 Git 仓库,当它被使用时所有这些都会发生变化。例如,当迁移正在进行时,在数据库中创建一个事务,而 Git 仓库正在被复制。如果备份发生在迁移的中间,Git 仓库可能是不完整的,尽管数据库声称它是完整的,因为它是在之后被转储的。避免这种竞争条件的唯一方法是在备份期间停止 Gitea 实例。 + ## 备份命令 (`dump`) 先转到git用户的权限: `su git`. 再Gitea目录运行 `./gitea dump`。一般会显示类似如下的输出: @@ -34,15 +40,43 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一 最后生成的 `gitea-dump-1482906742.zip` 文件将会包含如下内容: -* `custom` - 所有保存在 `custom/` 目录下的配置和自定义的文件。 -* `data` - 数据目录下的所有内容不包含使用文件session的文件。该目录包含 `attachments`, `avatars`, `lfs`, `indexers`, 如果使用sqlite 还会包含 sqlite 数据库文件。 +* `app.ini` - 如果原先存储在默认的 custom/ 目录之外,则是配置文件的可选副本 +* `custom/` - 所有保存在 `custom/` 目录下的配置和自定义的文件。 +* `data/` - 数据目录(APP_DATA_PATH),如果使用文件会话,则不包括会话。该目录包括 `attachments`、`avatars`、`lfs`、`indexers`、如果使用 SQLite 则包括 SQLite 文件。 +* `repos/` - 仓库目录的完整副本。 * `gitea-db.sql` - 数据库dump出来的 SQL。 -* `gitea-repo.zip` - Git仓库压缩文件。 * `log/` - Logs文件,如果用作迁移不是必须的。 中间备份文件将会在临时目录进行创建,如果您要重新指定临时目录,可以用 `--tempdir` 参数,或者用 `TMPDIR` 环境变量。 -## Restore Command (`restore`) +## 备份数据库 + +`gitea dump` 创建的 SQL 转储使用 XORM,Gitea 管理员可能更喜欢使用本地的 MySQL 和 PostgreSQL 转储工具。使用 XORM 转储数据库时仍然存在一些问题,可能会导致在尝试恢复时出现问题。 + +```sh +# mysql +mysqldump -u$USER -p$PASS --database $DATABASE > gitea-db.sql +# postgres +pg_dump -U $USER $DATABASE > gitea-db.sql +``` + +### 使用Docker (`dump`) + +在使用 Docker 时,使用 `dump` 命令有一些注意事项。 + +必须以 `gitea/conf/app.ini` 中指定的 `RUN_USER = <OS_USERNAME>` 执行该命令;并且,为了让备份文件夹的压缩过程能够顺利执行,`docker exec` 命令必须在 `--tempdir` 内部执行。 + +示例: + +```none +docker exec -u <OS_USERNAME> -it -w <--tempdir> $(docker ps -qf 'name=^<NAME_OF_DOCKER_CONTAINER>$') bash -c '/usr/local/bin/gitea dump -c </path/to/app.ini>' +``` + +\*注意:`--tempdir` 指的是 Gitea 使用的 Docker 环境的临时目录;如果您没有指定自定义的 `--tempdir`,那么 Gitea 将使用 `/tmp` 或 Docker 容器的 `TMPDIR` 环境变量。对于 `--tempdir`,请相应调整您的 `docker exec` 命令选项。 + +结果应该是一个文件,存储在指定的 `--tempdir` 中,类似于:`gitea-dump-1482906742.zip` + +## 恢复命令 (`restore`) 当前还没有恢复命令,恢复需要人工进行。主要是把文件和数据库进行恢复。 @@ -51,10 +85,10 @@ Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一 ```sh unzip gitea-dump-1610949662.zip cd gitea-dump-1610949662 -mv data/conf/app.ini /etc/gitea/conf/app.ini +mv app.ini /etc/gitea/conf/app.ini mv data/* /var/lib/gitea/data/ mv log/* /var/lib/gitea/log/ -mv repos/* /var/lib/gitea/repositories/ +mv repos/* /var/lib/gitea/gitea-repositories/ chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea # mysql @@ -66,3 +100,55 @@ psql -U $USER -d $DATABASE < gitea-db.sql service gitea restart ``` + +如果安装方式发生了变化(例如 二进制 -> Docker),或者 Gitea 安装到了与之前安装不同的目录,则需要重新生成仓库 Git 钩子。 + +在 Gitea 运行时,并从 Gitea 二进制文件所在的目录执行:`./gitea admin regenerate hooks` + +这样可以确保仓库 Git 钩子中的应用程序和配置文件路径与当前安装一致。如果这些路径没有更新,仓库的 `push` 操作将失败。 + +### 使用 Docker (`restore`) + +在基于 Docker 的 Gitea 实例中,也没有恢复命令的支持。恢复过程与前面描述的步骤相同,但路径不同。 + +示例: + +```sh +# 在容器中打开 bash 会话 +docker exec --user git -it 2a83b293548e bash +# 在容器内解压您的备份文件 +unzip gitea-dump-1610949662.zip +cd gitea-dump-1610949662 +# 恢复 Gitea 数据 +mv data/* /data/gitea +# 恢复仓库本身 +mv repos/* /data/git/gitea-repositories/ +# 调整文件权限 +chown -R git:git /data +# 重新生成 Git 钩子 +/usr/local/bin/gitea -c '/data/gitea/conf/app.ini' admin regenerate hooks +``` + +Gitea 容器中的默认用户是 `git`(1000:1000)。请用您的 Gitea 容器 ID 或名称替换 `2a83b293548e`。 + +### 使用 Docker-rootless (`restore`) + +在 Docker-rootless 容器中的恢复工作流程只是要使用的目录不同: + +```sh +# 在容器中打开 bash 会话 +docker exec --user git -it 2a83b293548e bash +# 在容器内解压您的备份文件 +unzip gitea-dump-1610949662.zip +cd gitea-dump-1610949662 +# 恢复 app.ini +mv data/conf/app.ini /etc/gitea/app.ini +# 恢复 Gitea 数据 +mv data/* /var/lib/gitea +# 恢复仓库本身 +mv repos/* /var/lib/gitea/git/gitea-repositories +# 调整文件权限 +chown -R git:git /etc/gitea/app.ini /var/lib/gitea +# 重新生成 Git 钩子 +/usr/local/bin/gitea -c '/etc/gitea/app.ini' admin regenerate hooks +``` diff --git a/docs/content/administration/cmd-embedded.zh-cn.md b/docs/content/administration/cmd-embedded.zh-cn.md index 4570bb58a3..a2df1aa2f5 100644 --- a/docs/content/administration/cmd-embedded.zh-cn.md +++ b/docs/content/administration/cmd-embedded.zh-cn.md @@ -37,7 +37,7 @@ gitea embedded list [--include-vendored] [patterns...] - 列出所有模板文件,无论在哪个虚拟目录下:`**.tmpl` - 列出所有邮件模板文件:`templates/mail/**.tmpl` -- 列出 `public/img` 目录下的所有文件:`public/img/**` +列出 `public/assets/img` 目录下的所有文件:`public/assets/img/**` 不要忘记为模式使用引号,因为空格、`*` 和其他字符可能对命令行解释器有特殊含义。 @@ -49,8 +49,8 @@ gitea embedded list [--include-vendored] [patterns...] ```sh $ gitea embedded list '**openid**' -public/img/auth/openid_connect.svg -public/img/openid-16x16.png +public/assets/img/auth/openid_connect.svg +public/assets/img/openid-16x16.png templates/user/auth/finalize_openid.tmpl templates/user/auth/signin_openid.tmpl templates/user/auth/signup_openid_connect.tmpl diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 33732d080b..9de7511964 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -126,7 +126,7 @@ In addition, there is _`StaticRootPath`_ which can be set as a built-in at build keywords used in Pull Request comments to automatically close a related issue - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen a related issue -- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash` +- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only` - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit. Only used if `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES` is `true`. - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list @@ -458,6 +458,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`. - `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071). - `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically. +- `SLOW_QUERY_THRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger. [^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details. @@ -517,13 +518,21 @@ And the following unique queues: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. +- `USER_DISABLED_FEATURES`: **_empty_** Disabled features for users, could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys` and more features can be added in future. + - `deletion`: User cannot delete their own account. + - `manage_ssh_keys`: User cannot configure ssh keys. + - `manage_gpg_keys`: User cannot configure gpg keys. +- `EXTERNAL_USER_DISABLE_FEATURES`: **_empty_**: Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. + - `deletion`: User cannot delete their own account. + - `manage_ssh_keys`: User cannot configure ssh keys. + - `manage_gpg_keys`: User cannot configure gpg keys. ## Security (`security`) - `INSTALL_LOCK`: **false**: Controls access to the installation page. When set to "true", the installation page is not accessible. - `SECRET_KEY`: **\<random at every install\>**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore. - `SECRET_KEY_URI`: **_empty_**: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_key`). It shouldn't be lost like SECRET_KEY. -- `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days. +- `LOGIN_REMEMBER_DAYS`: **31**: How long to remember that a user is logged in before requiring relogin (in days). - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication information. - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**: Header name for reverse proxy @@ -585,7 +594,7 @@ And the following unique queues: ## OpenID (`openid`) -- `ENABLE_OPENID_SIGNIN`: **false**: Allow authentication in via OpenID. +- `ENABLE_OPENID_SIGNIN`: **true**: Allow authentication in via OpenID. - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**: Allow registering via OpenID. - `WHITELISTED_URIS`: **_empty_**: If non-empty, list of POSIX regex patterns matching OpenID URI's to permit. @@ -827,7 +836,7 @@ Default templates for project boards: ## Issue and pull request attachments (`attachment`) - `ENABLED`: **true**: Whether issue and pull request attachments are enabled. -- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. +- `ALLOWED_TYPES`: **.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**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. - `MAX_SIZE`: **2048**: Maximum size (MB). - `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once. - `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` @@ -1401,7 +1410,7 @@ PROXY_HOSTS = *.github.com - `ZOMBIE_TASK_TIMEOUT`: **10m**: Timeout to stop the task which have running status, but haven't been updated for a long time - `ENDLESS_TASK_TIMEOUT`: **3h**: Timeout to stop the tasks which have running status and continuous updates, but don't end for a long time - `ABANDONED_JOB_TIMEOUT`: **24h**: Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time -- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message to skip executing the corresponding actions workflow +- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. For example, `uses: actions/checkout@v4` means `https://github.com/actions/checkout@v4` since the value of `DEFAULT_ACTIONS_URL` is `github`. @@ -1420,5 +1429,6 @@ Like `uses: https://gitea.com/actions/checkout@v4` or `uses: http://your-git-ser - `SHOW_FOOTER_VERSION`: **true**: Show Gitea and Go version information in the footer. - `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: Show time of template execution in the footer. +- `SHOW_FOOTER_POWERED_BY`: **true**: Show the "powered by" text in the footer. - `ENABLE_SITEMAP`: **true**: Generate sitemap. - `ENABLE_FEED`: **true**: Enable/Disable RSS/Atom feed. diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 2cee70daab..759f39b576 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -29,7 +29,7 @@ menu: [ini](https://github.com/go-ini/ini/#recursive-values) 这里的说明。 标注了 :exclamation: 的配置项表明除非你真的理解这个配置项的意义,否则最好使用默认值。 -在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`enviroment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。 +在下面的默认值中,`$XYZ`代表环境变量`XYZ`的值(详见:`environment-to-ini`)。 _`XxYyZz`_是指默认配置的一部分列出的值。这些在 app.ini 文件中不起作用,仅在此处列出作为文档说明。 包含`#`或者`;`的变量必须使用引号(`` ` ``或者`""""`)包裹,否则会被解析为注释。 @@ -125,7 +125,7 @@ menu: - `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: 在拉取请求评论中用于自动关闭相关问题的关键词列表。 - `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: 在拉取请求评论中用于自动重新打开相关问题的 关键词列表。 -- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash` +- `DEFAULT_MERGE_STYLE`: **merge**: 设置创建仓库的默认合并方式,可选: `merge`, `rebase`, `rebase-merge`, `squash`, `fast-forward-only` - `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: 在默认合并消息中,对于`squash`提交,最多包括此数量的提交。设置为 -1 以包括所有提交。 - `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: 在默认的合并消息中,对于`squash`提交,限制提交消息的大小。设置为 `-1`以取消限制。仅在`POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`为`true`时使用。 - `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: 在默认合并消息中,对于`squash`提交,遍历所有提交以包括所有作者的`Co-authored-by`,否则仅使用限定列表中的作者。 @@ -497,13 +497,17 @@ Gitea 创建以下非唯一队列: - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**:用户电子邮件通知的默认配置(用户可配置)。选项:enabled、onmention、disabled - `DISABLE_REGULAR_ORG_CREATION`: **false**:禁止普通(非管理员)用户创建组织。 +- `USER_DISABLED_FEATURES`:**_empty_** 禁用的用户特性,当前允许为空或者 `deletion`,`manage_ssh_keys`, `manage_gpg_keys` 未来可以增加更多设置。 + - `deletion`: 用户不能通过界面或者API删除他自己。 + - `manage_ssh_keys`: 用户不能通过界面或者API配置SSH Keys。 + - `manage_gpg_keys`: 用户不能配置 GPG 密钥。 ## 安全性 (`security`) - `INSTALL_LOCK`: **false**:控制是否能够访问安装向导页面,设置为 `true` 则禁止访问安装向导页面。 - `SECRET_KEY`: **\<每次安装时随机生成\>**:全局服务器安全密钥。这个密钥非常重要,如果丢失将无法解密加密的数据(例如 2FA)。 - `SECRET_KEY_URI`: **_empty_**:与定义 `SECRET_KEY` 不同,此选项可用于使用存储在文件中的密钥(示例值:`file:/etc/gitea/secret_key`)。它不应该像 `SECRET_KEY` 一样容易丢失。 -- `LOGIN_REMEMBER_DAYS`: **7**:Cookie 保存时间,单位为天。 +- `LOGIN_REMEMBER_DAYS`: **31**:在要求重新登录之前,记住用户的登录状态多长时间(以天为单位)。 - `COOKIE_REMEMBER_NAME`: **gitea\_incredible**:保存自动登录信息的 Cookie 名称。 - `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**:反向代理认证的 HTTP 头部名称,用于提供用户信息。 - `REVERSE_PROXY_AUTHENTICATION_EMAIL`: **X-WEBAUTH-EMAIL**:反向代理认证的 HTTP 头部名称,用于提供邮箱信息。 @@ -558,7 +562,7 @@ Gitea 创建以下非唯一队列: ## OpenID (`openid`) -- `ENABLE_OPENID_SIGNIN`: **false**:允许通过OpenID进行身份验证。 +- `ENABLE_OPENID_SIGNIN`: **true**:允许通过OpenID进行身份验证。 - `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**:允许通过OpenID进行注册。 - `WHITELISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于允许访问。 - `BLACKLISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于阻止访问。 @@ -778,7 +782,7 @@ Gitea 创建以下非唯一队列: ## 工单和合并请求的附件 (`attachment`) - `ENABLED`: **true**: 是否允许用户上传附件。 -- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。 +- `ALLOWED_TYPES`: **.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**: 允许的文件扩展名(`.zip`)、mime 类型(`text/plain`)或通配符类型(`image/*`、`audio/*`、`video/*`)的逗号分隔列表。空值或 `*/*` 允许所有类型。 - `MAX_SIZE`: **2048**: 附件的最大限制(MB)。 - `MAX_FILES`: **5**: 一次最多上传的附件数量。 - `STORAGE_TYPE`: **local**: 附件的存储类型,`local` 表示本地磁盘,`minio` 表示兼容 S3 的对象存储服务,如果未设置将使用默认值 `local` 或其他在 `[storage.xxx]` 中定义的名称。 @@ -1349,5 +1353,6 @@ PROXY_HOSTS = *.github.com - `SHOW_FOOTER_VERSION`: **true**: 在页面底部显示Gitea的版本。 - `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: 在页脚显示模板执行的时间。 +- `SHOW_FOOTER_POWERED_BY`: **true**: 在页脚显示“由...提供动力”的文本。 - `ENABLE_SITEMAP`: **true**: 生成sitemap. - `ENABLE_FEED`: **true**: 是否启用RSS/Atom diff --git a/docs/content/administration/customizing-gitea.en-us.md b/docs/content/administration/customizing-gitea.en-us.md index d122fb4bfa..7efddb2824 100644 --- a/docs/content/administration/customizing-gitea.en-us.md +++ b/docs/content/administration/customizing-gitea.en-us.md @@ -284,7 +284,7 @@ syntax and shouldn't be touched without fully understanding these components. Google Analytics, Matomo (previously Piwik), and other analytics services can be added to Gitea. To add the tracking code, refer to the `Other additions to the page` section of this document, and add the JavaScript to the `$GITEA_CUSTOM/templates/custom/header.tmpl` file. -## Customizing gitignores, labels, licenses, locales, and readmes. +## Customizing gitignores, labels, licenses, locales, and readmes Place custom files in corresponding sub-folder under `custom/options`. diff --git a/docs/content/administration/https-support.en-us.md b/docs/content/administration/https-support.en-us.md index 4e18722ddf..981a29bd85 100644 --- a/docs/content/administration/https-support.en-us.md +++ b/docs/content/administration/https-support.en-us.md @@ -35,7 +35,7 @@ CERT_FILE = cert.pem KEY_FILE = key.pem ``` -Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to estalbish the trust relationship. +Note that if your certificate is signed by a third party certificate authority (i.e. not self-signed), then cert.pem should contain the certificate chain. The server certificate must be the first entry in cert.pem, followed by the intermediaries in order (if any). The root certificate does not have to be included because the connecting client must already have it in order to establish the trust relationship. To learn more about the config values, please checkout the [Config Cheat Sheet](administration/config-cheat-sheet.md#server-server). For the `CERT_FILE` or `KEY_FILE` field, the file path is relative to the `GITEA_CUSTOM` environment variable when it is a relative path. It can be an absolute path as well. diff --git a/docs/content/administration/mail-templates.en-us.md b/docs/content/administration/mail-templates.en-us.md index 32b352da4b..8e4e416e8d 100644 --- a/docs/content/administration/mail-templates.en-us.md +++ b/docs/content/administration/mail-templates.en-us.md @@ -163,7 +163,7 @@ clients don't even support HTML, so they show the text version included in the g If the template fails to render, it will be noticed only at the moment the mail is sent. A default subject is used if the subject template fails, and whatever was rendered successfully -from the the _mail body_ is used, disregarding the rest. +from the _mail body_ is used, disregarding the rest. Please check [Gitea's logs](administration/logging-config.md) for error messages in case of trouble. @@ -222,9 +222,9 @@ Please check [Gitea's logs](administration/logging-config.md) for error messages <a href="{{.Link}}">{{.Repo}}#{{.Issue.Index}}</a>. </p> {{if not (eq .Body "")}} - <h3>Message content:</h3> + <h3>Message content</h3> <hr> - {{.Body | Str2html}} + {{.Body}} {{end}} </p> <hr> @@ -245,7 +245,7 @@ This template produces something along these lines: > [@rhonda](#) (Rhonda Myers) updated [mike/stuff#38](#). > -> #### Message content: +> #### Message content > > \_********************************\_******************************** > @@ -259,20 +259,20 @@ This template produces something along these lines: The template system contains several functions that can be used to further process and format the messages. Here's a list of some of them: -| Name | Parameters | Available | Usage | -| ---------------- | ----------- | --------- | --------------------------------------------------------------------------- | -| `AppUrl` | - | Any | Gitea's URL | -| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" | -| `AppDomain` | - | Any | Gitea's host name | -| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed | -| `Str2html` | string | Body only | Sanitizes text by removing any HTML tags from it. | -| `Safe` | string | Body only | Takes the input as HTML; can be used for `.ReviewComments.RenderedContent`. | +| Name | Parameters | Available | Usage | +| ---------------- | ----------- | --------- | ------------------------------------------------------------------- | +| `AppUrl` | - | Any | Gitea's URL | +| `AppName` | - | Any | Set from `app.ini`, usually "Gitea" | +| `AppDomain` | - | Any | Gitea's host name | +| `EllipsisString` | string, int | Any | Truncates a string to the specified length; adds ellipsis as needed | +| `SanitizeHTML` | string | Body only | Sanitizes text by removing any dangerous HTML tags from it | +| `SafeHTML` | string | Body only | Takes the input as HTML, can be used for outputing raw HTML content | These are _functions_, not metadata, so they have to be used: ```html -Like this: {{Str2html "Escape<my>text"}} -Or this: {{"Escape<my>text" | Str2html}} +Like this: {{SanitizeHTML "Escape<my>text"}} +Or this: {{"Escape<my>text" | SanitizeHTML}} Or this: {{AppUrl}} But not like this: {{.AppUrl}} ``` diff --git a/docs/content/administration/mail-templates.zh-cn.md b/docs/content/administration/mail-templates.zh-cn.md index 588f0b2ccb..3c7c2a9397 100644 --- a/docs/content/administration/mail-templates.zh-cn.md +++ b/docs/content/administration/mail-templates.zh-cn.md @@ -207,7 +207,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/ {{if not (eq .Body "")}} <h3>消息内容:</h3> <hr> - {{.Body | Str2html}} + {{.Body}} {{end}} </p> <hr> @@ -228,7 +228,7 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/ > [@rhonda](#)(Rhonda Myers)更新了 [mike/stuff#38](#)。 > -> #### 消息内容: +> #### 消息内容 > > \_********************************\_******************************** > @@ -242,20 +242,20 @@ _主题_ 和 _邮件正文_ 由 [Golang的模板引擎](https://go.dev/pkg/text/ 模板系统包含一些函数,可用于进一步处理和格式化消息。以下是其中一些函数的列表: -| 函数名 | 参数 | 可用于 | 用法 | -| ----------------- | ----------- | ------------ | --------------------------------------------------------------------------------- | -| `AppUrl` | - | 任何地方 | Gitea 的 URL | -| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" | -| `AppDomain` | - | 任何地方 | Gitea 的主机名 | -| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 | -| `Str2html` | string | 仅正文部分 | 通过删除其中的 HTML 标签对文本进行清理 | -| `Safe` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于 `.ReviewComments.RenderedContent` 等字段 | +| 函数名 | 参数 | 可用于 | 用法 | +|------------------| ----------- | ------------ | ------------------------------ | +| `AppUrl` | - | 任何地方 | Gitea 的 URL | +| `AppName` | - | 任何地方 | 从 `app.ini` 中设置,通常为 "Gitea" | +| `AppDomain` | - | 任何地方 | Gitea 的主机名 | +| `EllipsisString` | string, int | 任何地方 | 将字符串截断为指定长度;根据需要添加省略号 | +| `SanitizeHTML` | string | 仅正文部分 | 通过删除其中的危险 HTML 标签对文本进行清理 | +| `SafeHTML` | string | 仅正文部分 | 将输入作为 HTML 处理;可用于输出原始的 HTML 内容 | 这些都是 _函数_,而不是元数据,因此必须按以下方式使用: ```html -像这样使用: {{Str2html "Escape<my>text"}} -或者这样使用: {{"Escape<my>text" | Str2html}} +像这样使用: {{SanitizeHTML "Escape<my>text"}} +或者这样使用: {{"Escape<my>text" | SanitizeHTML}} 或者这样使用: {{AppUrl}} 但不要像这样使用: {{.AppUrl}} ``` diff --git a/docs/content/administration/repo-indexer.en-us.md b/docs/content/administration/repo-indexer.en-us.md index 6dec2d63fa..aa82222911 100644 --- a/docs/content/administration/repo-indexer.en-us.md +++ b/docs/content/administration/repo-indexer.en-us.md @@ -17,6 +17,12 @@ menu: # Repository indexer +## Builtin repository code search without indexer + +Users could do repository-level code search without setting up a repository indexer. +The builtin code search is based on the `git grep` command, which is fast and efficient for small repositories. +Better code search support could be achieved by setting up the repository indexer. + ## Setting up the repository indexer Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md): diff --git a/docs/content/contributing/guidelines-backend.en-us.md b/docs/content/contributing/guidelines-backend.en-us.md index 084b3886e8..3159a5ff7d 100644 --- a/docs/content/contributing/guidelines-backend.en-us.md +++ b/docs/content/contributing/guidelines-backend.en-us.md @@ -101,6 +101,10 @@ i.e. `services/user`, `models/repository`. Since there are some packages which use the same package name, it is possible that you find packages like `modules/user`, `models/user`, and `services/user`. When these packages are imported in one Go file, it's difficult to know which package we are using and if it's a variable name or an import name. So, we always recommend to use import aliases. To differ from package variables which are commonly in camelCase, just use **snake_case** for import aliases. i.e. `import user_service "code.gitea.io/gitea/services/user"` +### Implementing `io.Closer` + +If a type implements `io.Closer`, calling `Close` multiple times must not fail or `panic` but return an error or `nil`. + ### Important Gotchas - Never write `x.Update(exemplar)` without an explicit `WHERE` clause: diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md index edd89e1231..efeaf38bb2 100644 --- a/docs/content/contributing/guidelines-frontend.en-us.md +++ b/docs/content/contributing/guidelines-frontend.en-us.md @@ -34,7 +34,7 @@ The source files can be found in the following directories: We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) -### Gitea specific guidelines: +### Gitea specific guidelines 1. Every feature (Fomantic-UI/jQuery module) should be put in separate files/directories. 2. HTML ids and classes should use kebab-case, it's preferred to contain 2-3 feature related keywords. @@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h 9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided. 10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event. 11. Custom event names are recommended to use `ce-` prefix. -12. Gitea's tailwind-style CSS classes use `gt-` prefix (`gt-relative`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). +12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-word-break`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). 13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided. ### Accessibility / ARIA @@ -118,7 +118,7 @@ However, there are still some special cases, so the current guideline is: ### Show/Hide Elements * Vue components are recommended to use `v-if` and `v-show` to show/hide elements. -* Go template code should use Gitea's `.gt-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.gt-hidden`'s comment. +* Go template code should use `.tw-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.tw-hidden`'s comment. ### Styles and Attributes in Go HTML Template diff --git a/docs/content/contributing/guidelines-frontend.zh-cn.md b/docs/content/contributing/guidelines-frontend.zh-cn.md index 66a4d4b4d6..394097b259 100644 --- a/docs/content/contributing/guidelines-frontend.zh-cn.md +++ b/docs/content/contributing/guidelines-frontend.zh-cn.md @@ -34,7 +34,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。 我们推荐使用[Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html)和[Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html)。 -## Gitea 特定准则: +## Gitea 特定准则 1. 每个功能(Fomantic-UI/jQuery 模块)应放在单独的文件/目录中。 2. HTML 的 id 和 class 应使用 kebab-case,最好包含2-3个与功能相关的关键词。 @@ -47,12 +47,13 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。 11. 推荐使用自定义事件名称前缀`ce-`。 -12. Gitea 的 tailwind-style CSS 类使用`gt-`前缀(`gt-relative`),而 Gitea 自身的私有框架级 CSS 类使用`g-`前缀(`g-modal-confirm`)。 +12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-word-break`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。 +13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。 ### 可访问性 / ARIA 在历史上,Gitea大量使用了可访问性不友好的框架 Fomantic UI。 -Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`aria.md`), +Gitea 使用一些补丁使 Fomantic UI 更具可访问性(参见 `aria.md`), 但仍然存在许多问题需要大量的工作和时间来修复。 ### 框架使用 @@ -64,18 +65,21 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari * Vue + Vanilla JS * Fomantic-UI(jQuery) +* htmx (部分页面重新加载其他静态组件) * Vanilla JS 不推荐的实现方式: * Vue + Fomantic-UI(jQuery) * jQuery + Vanilla JS +* htmx + 任何其他需要大量 JavaScript 代码或不必要的功能,如 htmx 脚本 (`hx-on`) 为了保持界面一致,Vue 组件可以使用 Fomantic-UI 的 CSS 类。 尽管不建议混合使用不同的框架, +我们使用 htmx 进行简单的交互。您可以在此 [PR](https://github.com/go-gitea/gitea/pull/28908) 中查看一个简单交互的示例,其中应使用 htmx。如果您需要更高级的反应性,请不要使用 htmx,请使用其他框架(Vue/Vanilla JS)。 但如果混合使用是必要的,并且代码设计良好且易于维护,也可以工作。 -### async 函数 +### `async` 函数 只有当函数内部存在`await`调用或返回`Promise`时,才将函数标记为`async`。 @@ -91,6 +95,12 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari 这是有意为之的,我们想调用异步函数并忽略Promise。 一些 lint 规则和 IDE 也会在未处理返回的 Promise 时发出警告。 +### 获取数据 + +要获取数据,请使用`modules/fetch.js`中的包装函数`GET`、`POST`等。他们 +接受内容的`data`选项,将自动设置 CSRF 令牌并返回 +[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)。 + ### HTML 属性和 dataset 禁止使用`dataset`,它的驼峰命名行为使得搜索属性变得困难。 @@ -107,7 +117,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari ### 显示/隐藏元素 * 推荐在Vue组件中使用`v-if`和`v-show`来显示/隐藏元素。 -* Go 模板代码应使用 Gitea 的 `.gt-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.gt-hidden`的注释以获取更多详细信息。 +* Go 模板代码应使用 `.tw-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.tw-hidden`的注释以获取更多详细信息。 ### Go HTML 模板中的样式和属性 @@ -132,3 +142,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari ### Vue3 和 JSX Gitea 现在正在使用 Vue3。我们决定不引入 JSX,以保持 HTML 代码和 JavaScript 代码分离。 + +### UI示例 + +Gitea 使用一些自制的 UI 元素并自定义其他元素,以将它们更好地集成到通用 UI 方法中。当在开发模式(`RUN_MODE=dev`)下运行 Gitea 时,在 `http(s)://your-gitea-url:port/devtest` 下会提供一个包含一些标准化 UI 示例的页面。 diff --git a/docs/content/development/api-usage.zh-cn.md b/docs/content/development/api-usage.zh-cn.md index 96c1997294..d7aca16f7f 100644 --- a/docs/content/development/api-usage.zh-cn.md +++ b/docs/content/development/api-usage.zh-cn.md @@ -60,7 +60,7 @@ curl "http://localhost:4000/api/v1/repos/test1/test1/issues" \ `/users/:name/tokens` 是一个特殊的接口,需要您使用 basic authentication 进行认证,具体原因在 issue 中 [#3842](https://github.com/go-gitea/gitea/issues/3842#issuecomment-397743346) 有所提及,使用方法如下所示: -### 使用 Basic authentication 认证: +### 使用 Basic authentication 认证 ``` $ curl --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens diff --git a/docs/content/development/hacking-on-gitea.en-us.md b/docs/content/development/hacking-on-gitea.en-us.md index 4b132c49d9..004e803827 100644 --- a/docs/content/development/hacking-on-gitea.en-us.md +++ b/docs/content/development/hacking-on-gitea.en-us.md @@ -214,7 +214,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 ### Building and adding SVGs -SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory. +SVG icons are built using the `make svg` target which compiles the icon sources into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory. ### Building the Logo @@ -243,10 +243,10 @@ documentation using: make generate-swagger ``` -You should validate your generated Swagger file and spell-check it with: +You should validate your generated Swagger file: ```bash -make swagger-validate misspell-check +make swagger-validate ``` You should commit the changed swagger JSON file. The continuous integration @@ -333,14 +333,9 @@ Documentation for the website is found in `docs/`. If you change this you can test your changes to ensure that they pass continuous integration using: ```bash -# from the docs directory within Gitea -make trans-copy clean build +make lint-md ``` -You will require a copy of [Hugo](https://gohugo.io/) to run this task. Please -note: this may generate a number of untracked Git objects, which will need to -be cleaned up. - ## Visual Studio Code A `launch.json` and `tasks.json` are provided within `contrib/ide/vscode` for diff --git a/docs/content/development/hacking-on-gitea.zh-cn.md b/docs/content/development/hacking-on-gitea.zh-cn.md index 364bbf1ffe..7dfea30538 100644 --- a/docs/content/development/hacking-on-gitea.zh-cn.md +++ b/docs/content/development/hacking-on-gitea.zh-cn.md @@ -201,7 +201,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 ### 构建和添加 SVGs -SVG 图标是使用 `make svg` 目标构建的,该目标将 `build/generate-svg.js` 中定义的图标源编译到输出目录 `public/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。 +SVG 图标是使用 `make svg` 命令构建的,该命令将图标资源编译到输出目录 `public/assets/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。 ### 构建 Logo @@ -228,10 +228,10 @@ Gitea Logo的 PNG 和 SVG 版本是使用 `TAGS="gitea" make generate-images` make generate-swagger ``` -您应该验证生成的 Swagger 文件并使用以下命令对其进行拼写检查: +您应该验证生成的 Swagger 文件: ```bash -make swagger-validate misspell-check +make swagger-validate ``` 您应该提交更改后的 swagger JSON 文件。持续集成服务器将使用以下方法检查是否已完成: @@ -307,13 +307,9 @@ TAGS="bindata sqlite sqlite_unlock_notify" make build test-sqlite 该网站的文档位于 `docs/` 中。如果你改变了文档内容,你可以使用以下测试方法进行持续集成: ```bash -# 来自 Gitea 中的 docs 目录 -make trans-copy clean build +make lint-md ``` -运行此任务依赖于 [Hugo](https://gohugo.io/)。请注意:这可能会生成一些未跟踪的 Git 对象, -需要被清理干净。 - ## Visual Studio Code `contrib/ide/vscode` 中为 Visual Studio Code 提供了 `launch.json` 和 `tasks.json`。查看 diff --git a/docs/content/help/faq.en-us.md b/docs/content/help/faq.en-us.md index 5ea2c10f5e..b3b0980125 100644 --- a/docs/content/help/faq.en-us.md +++ b/docs/content/help/faq.en-us.md @@ -221,9 +221,11 @@ Our translations are currently crowd-sourced on our [Crowdin project](https://cr Whether you want to change a translation or add a new one, it will need to be there as all translations are overwritten in our CI via the Crowdin integration. -## Push Hook / Webhook aren't running +## Push Hook / Webhook / Actions aren't running -If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook, there are a few possibilities: +If you can push but can't see push activities on the home dashboard, or the push doesn't trigger webhook and Actions workflows, it's likely that the git hooks are not working. + +There are a few possibilities: 1. The git hooks are out of sync: run "Resynchronize pre-receive, update and post-receive hooks of all repositories" on the site admin panel 2. The git repositories (and hooks) are stored on some filesystems (ex: mounted by NAS) which don't support script execution, make sure the filesystem supports `chmod a+x any-script` diff --git a/docs/content/help/faq.zh-cn.md b/docs/content/help/faq.zh-cn.md index b8dd3cd180..25230df70b 100644 --- a/docs/content/help/faq.zh-cn.md +++ b/docs/content/help/faq.zh-cn.md @@ -225,9 +225,11 @@ Gitea还提供了自己的SSH服务器,用于在SSHD不可用时使用。 无论您想要更改翻译还是添加新的翻译,都需要在Crowdin集成中进行,因为所有翻译都会被CI覆盖。 -## 推送钩子/ Webhook未运行 +## 推送钩子/ Webhook / Actions 未运行 -如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发Webhook,有几种可能性: +如果您可以推送但无法在主页仪表板上看到推送活动,或者推送不触发 Webhook 和 Actions,可能是 git 钩子不工作而导致的。 + +这可能是由于以下原因: 1. Git钩子不同步:在站点管理面板上运行“重新同步所有仓库的pre-receive、update和post-receive钩子” 2. Git仓库(和钩子)存储在一些不支持脚本执行的文件系统上(例如由NAS挂载),请确保文件系统支持`chmod a+x any-script` diff --git a/docs/content/help/support.zh-cn.md b/docs/content/help/support.zh-cn.md index de56d8abe0..91b37c586c 100644 --- a/docs/content/help/support.zh-cn.md +++ b/docs/content/help/support.zh-cn.md @@ -15,11 +15,64 @@ menu: identifier: "support" --- -## 需要帮助? +# 支持选项 -如果您在使用或者开发过程中遇到问题,请到以下渠道咨询: +- [付费商业支持](https://about.gitea.com/) +- [Discord](https://discord.gg/Gitea) +- [Discourse 论坛](https://discourse.gitea.io/) +- [Matrix](https://matrix.to/#/#gitea-space:matrix.org) + - 注意:大多数 Matrix 频道都与 Discord 中的对应频道桥接,可能在桥接过程中会出现一定程度的不稳定性。 +- 中文支持 + - [Discourse 中文分类](https://discourse.gitea.io/c/5-category/5) + - QQ 群 328432459 -- 到 [GitHub Issue](https://github.com/go-gitea/gitea/issues) 提问(因为项目维护人员来自世界各地,为保证沟通顺畅,请使用英文提问) -- 中文问题到 [Gitea 论坛](https://discourse.gitea.io/c/5-category/5) 提问 -- 访问 [Discord Gitea 聊天室 - 英文](https://discord.gg/Gitea) -- 加入 QQ群 328432459 获得进一步的支持 +# Bug 报告 + +如果您发现了 Bug,请在 GitHub 上 [创建一个问题](https://github.com/go-gitea/gitea/issues)。 + +**注意:** 在请求支持时,可能需要准备以下信息,以便帮助者获得所需的所有信息: + +1. 您的 `app.ini`(将任何敏感数据进行必要的清除)。 +2. 您看到的任何错误消息。 +3. Gitea 日志以及与情况相关的所有其他日志。 + - 收集 `trace` / `debug` 级别的日志更有用(参见下一节)。 + - 在使用 systemd 时,使用 `journalctl --lines 1000 --unit gitea` 收集日志。 + - 在使用 Docker 时,使用 `docker logs --tail 1000 <gitea-container>` 收集日志。 +4. 可重现的步骤,以便他人能够更快速、更容易地重现和理解问题。 + - [try.gitea.io](https://try.gitea.io) 可用于重现问题。 +5. 如果遇到慢速/挂起/死锁等问题,请在出现问题时报告堆栈跟踪。 + 转到 "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report"。 + +# 高级 Bug 报告提示 + +## 更多日志的配置选项 + +默认情况下,日志以 `info` 级别输出到控制台。 +如果您需要设置日志级别和/或从文件中收集日志, +您只需将以下配置复制到您的 `app.ini` 中(删除所有其他 `[log]` 部分), +然后您将在 Gitea 的日志目录中找到 `*.log` 文件(默认为 `%(GITEA_WORK_DIR)/log`)。 + +```ini +; 要显示所有 SQL 日志,您还可以在 [database] 部分中设置 LOG_SQL=true +[log] +LEVEL=debug +MODE=console,file +``` + +## 使用命令行收集堆栈跟踪 + +Gitea 可以使用 Golang 的 pprof 处理程序和工具链来收集堆栈跟踪和其他运行时信息。 + +如果 Web UI 停止工作,您可以尝试通过命令行收集堆栈跟踪: + +1. 设置 app.ini: + + ``` + [server] + ENABLE_PPROF = true + ``` + +2. 重新启动 Gitea + +3. 尝试触发bug,当请求卡住一段时间,使用或浏览器访问:获取堆栈跟踪。 +`curl http://127.0.0.1:6060/debug/pprof/goroutine?debug=1` diff --git a/docs/content/installation/comparison.en-us.md b/docs/content/installation/comparison.en-us.md index 1ba4f7ecc2..3fb6561f31 100644 --- a/docs/content/installation/comparison.en-us.md +++ b/docs/content/installation/comparison.en-us.md @@ -87,6 +87,9 @@ _Symbols used in table:_ | Git Blame | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Visual comparison of image changes | ✓ | ✘ | ✓ | ? | ? | ? | ✘ | ✘ | +- Gitea has builtin repository-level code search +- Better code search support could be achieved by [using a repository indexer](administration/repo-indexer.md) + ## Issue Tracker | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE | diff --git a/docs/content/installation/database-preparation.zh-cn.md b/docs/content/installation/database-preparation.zh-cn.md index d651088395..3fde004a8c 100644 --- a/docs/content/installation/database-preparation.zh-cn.md +++ b/docs/content/installation/database-preparation.zh-cn.md @@ -17,7 +17,9 @@ menu: # 数据库准备 -在使用 Gitea 前,您需要准备一个数据库。Gitea 支持 PostgreSQL(>= 12)、MySQL(>= 8.0)、SQLite 和 MSSQL(>= 2012 SP4)这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite,则可以忽略本章内容。 +在使用 Gitea 前,您需要准备一个数据库。Gitea 支持 PostgreSQL(>= 12)、MySQL(>= 8.0)、MariaDB(>= 10.4)、SQLite(内置) 和 MSSQL(>= 2012 SP4)这几种数据库。本页将指导您准备数据库。由于 PostgreSQL 和 MySQL 在生产环境中被广泛使用,因此本文档将仅涵盖这两种数据库。如果您计划使用 SQLite,则可以忽略本章内容。 + +如果您使用不受支持的数据库版本,请通过 [联系我们](/help/support) 以获取有关我们的扩展支持的信息。我们可以为旧数据库提供测试和支持,并将这些修复集成到 Gitea 代码库中。 数据库实例可以与 Gitea 实例在相同机器上(本地数据库),也可以与 Gitea 实例在不同机器上(远程数据库)。 @@ -61,7 +63,9 @@ menu: 4. 使用 UTF-8 字符集和大小写敏感的排序规则创建数据库。 - Gitea 启动后会尝试把数据库修改为更合适的字符集,如果你想指定自己的字符集规则,可以在 app.ini 中设置 `[database].CHARSET_COLLATION`。 + `utf8mb4_bin` 是 MySQL/MariaDB 的通用排序规则。 + Gitea 启动后会尝试把数据库修改为更合适的字符集 (`utf8mb4_0900_as_cs` 或者 `uca1400_as_cs`) 并在可能的情况下更改数据库。 + 如果你想指定自己的字符集规则,可以在 `app.ini` 中设置 `[database].CHARSET_COLLATION`。 ```sql CREATE DATABASE giteadb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_bin'; @@ -85,7 +89,7 @@ menu: FLUSH PRIVILEGES; ``` -6. 通过 exit 退出数据库控制台。 +6. 通过 `exit` 退出数据库控制台。 7. 在您的 Gitea 服务器上,测试与数据库的连接: @@ -93,13 +97,13 @@ menu: mysql -u gitea -h 203.0.113.3 -p giteadb ``` - 其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 -h 选项。 + 其中 `gitea` 是数据库用户名,`giteadb` 是数据库名称,`203.0.113.3` 是数据库实例的 IP 地址。对于本地数据库,省略 `-h` 选项。 到此您应该能够连接到数据库了。 ## PostgreSQL -1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 listen_addresses 将 PostgreSQL 配置为监听您的 IP 地址: +1. 对于远程数据库设置,通过编辑数据库实例上的 postgresql.conf 文件中的 `listen_addresses` 将 `PostgreSQL` 配置为监听您的 IP 地址: ```ini listen_addresses = 'localhost, 203.0.113.3' diff --git a/docs/content/installation/from-source.en-us.md b/docs/content/installation/from-source.en-us.md index 601e074745..cd9fd56511 100644 --- a/docs/content/installation/from-source.en-us.md +++ b/docs/content/installation/from-source.en-us.md @@ -27,13 +27,7 @@ Next, [install Node.js with npm](https://nodejs.org/en/download/) which is required to build the JavaScript and CSS files. The minimum supported Node.js version is @minNodeVersion@ and the latest LTS version is recommended. -**Note**: When executing make tasks that require external tools, like -`make misspell-check`, Gitea will automatically download and build these as -necessary. To be able to use these, you must have the `"$GOPATH/bin"` directory -on the executable path. If you don't add the go bin directory to the -executable path, you will have to manage this yourself. - -**Note 2**: Go version @minGoVersion@ or higher is required. However, it is recommended to +**Note**: Go version @minGoVersion@ or higher is required. However, it is recommended to obtain the same version as our continuous integration, see the advice given in [Hacking on Gitea](development/hacking-on-gitea.md) diff --git a/docs/content/installation/from-source.zh-cn.md b/docs/content/installation/from-source.zh-cn.md index c2bd5785b2..3ff7efb4ed 100644 --- a/docs/content/installation/from-source.zh-cn.md +++ b/docs/content/installation/from-source.zh-cn.md @@ -21,9 +21,7 @@ menu: 接下来,[安装 Node.js 和 npm](https://nodejs.org/zh-cn/download/), 这是构建 JavaScript 和 CSS 文件所需的。最低支持的 Node.js 版本是 @minNodeVersion@,建议使用最新的 LTS 版本。 -**注意**:当执行需要外部工具的 make 任务(如`make misspell-check`)时,Gitea 将根据需要自动下载和构建这些工具。为了能够实现这个目的,你必须将`"$GOPATH/bin"`目录添加到可执行路径中。如果没有将 Go 的二进制目录添加到可执行路径中,你需要自行解决产生的问题。 - -**注意2**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。 +**注意**:需要 Go 版本 @minGoVersion@ 或更高版本。不过,建议获取与我们的持续集成(continuous integration, CI)相同的版本,请参阅在 [Hacking on Gitea](development/hacking-on-gitea.md) 中给出的建议。 ## 下载 diff --git a/docs/content/installation/windows-service.zh-cn.md b/docs/content/installation/windows-service.zh-cn.md index 985492b7e8..d5761d2c19 100644 --- a/docs/content/installation/windows-service.zh-cn.md +++ b/docs/content/installation/windows-service.zh-cn.md @@ -15,7 +15,7 @@ menu: identifier: "windows-service" --- -# 准备工作 +## 准备工作 在 C:\gitea\custom\conf\app.ini 中进行了以下更改: @@ -27,7 +27,7 @@ RUN_USER = COMPUTERNAME$ COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应。如果响应是 `USER-PC`,那么 `RUN_USER = USER-PC$`。 -## 使用绝对路径 +### 使用绝对路径 如果您使用 SQLite3,请将 `PATH` 更改为包含完整路径: @@ -36,7 +36,7 @@ COMPUTERNAME 是从命令行中运行 `echo %COMPUTERNAME%` 后得到的响应 PATH = c:/gitea/data/gitea.db ``` -# 注册为Windows服务 +## 注册为Windows服务 要注册为Windows服务,首先以Administrator身份运行 `cmd`,然后执行以下命令: @@ -48,7 +48,16 @@ sc.exe create gitea start= auto binPath= "\"C:\gitea\gitea.exe\" web --config \" 之后在控制面板打开 "Windows Services",搜索 "gitea",右键选择 "Run"。在浏览器打开 `http://localhost:3000` 就可以访问了。(如果你修改了端口,请访问对应的端口,3000是默认端口)。 -## 添加启动依赖项 +### 服务启动类型 + +据观察,在启动期间加载的系统上,Gitea 服务可能无法启动,并在 Windows 事件日志中记录超时。 +在这种情况下,将启动类型更改为`Automatic-Delayed`。这可以在服务创建期间完成,或者通过运行配置命令来完成。 + +``` +sc.exe config gitea start= delayed-auto +``` + +### 添加启动依赖项 要将启动依赖项添加到 Gitea Windows 服务(例如 Mysql、Mariadb),作为管理员,然后运行以下命令: diff --git a/docs/content/installation/with-docker.en-us.md b/docs/content/installation/with-docker.en-us.md index e67f5bccb2..a16d4a8d60 100644 --- a/docs/content/installation/with-docker.en-us.md +++ b/docs/content/installation/with-docker.en-us.md @@ -304,7 +304,8 @@ services: - GITEA__mailer__ENABLED=true - GITEA__mailer__FROM=${GITEA__mailer__FROM:?GITEA__mailer__FROM not set} - GITEA__mailer__PROTOCOL=smtps - - GITEA__mailer__HOST=${GITEA__mailer__HOST:?GITEA__mailer__HOST not set} + - GITEA__mailer__SMTP_ADDR=${GITEA__mailer__SMTP_ADDR:?GITEA__mailer__SMTP_ADDR not set} + - GITEA__mailer__SMTP_PORT=${GITEA__mailer__SMTP_PORT:?GITEA__mailer__SMTP_PORT not set} - GITEA__mailer__USER=${GITEA__mailer__USER:-apikey} - GITEA__mailer__PASSWD="""${GITEA__mailer__PASSWD:?GITEA__mailer__PASSWD not set}""" ``` @@ -545,7 +546,7 @@ In this option, the idea is that the host SSH uses an `AuthorizedKeysCommand` in ```bash cat <<"EOF" | sudo tee /home/git/docker-shell #!/bin/sh - /usr/bin/docker exec -i --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@" + /usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@" EOF sudo chmod +x /home/git/docker-shell sudo usermod -s /home/git/docker-shell git @@ -560,7 +561,7 @@ Add the following block to `/etc/ssh/sshd_config`, on the host: ```bash Match User git AuthorizedKeysCommandUser git - AuthorizedKeysCommand /usr/bin/docker exec -i gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k + AuthorizedKeysCommand /usr/bin/docker exec -i -u git gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k ``` (From 1.16.0 you will not need to set the `-c /data/gitea/conf/app.ini` option.) diff --git a/docs/content/usage/actions/act-runner.en-us.md b/docs/content/usage/actions/act-runner.en-us.md index 3fad9cbfe8..942d126919 100644 --- a/docs/content/usage/actions/act-runner.en-us.md +++ b/docs/content/usage/actions/act-runner.en-us.md @@ -120,6 +120,8 @@ A registration token can also be obtained from the gitea [command-line interface gitea --config /etc/gitea/app.ini actions generate-runner-token ``` +Tokens are valid for registering multiple runners, until they are revoked and replaced by a new token using the token reset link in the web interface. + ### Register the runner The act runner can be registered by running the following command: @@ -301,34 +303,3 @@ sudo systemctl enable act_runner --now ``` If using Docker, the `act_runner` user should also be added to the `docker` group before starting the service. Keep in mind that this effectively gives `act_runner` root access to the system [[1]](https://docs.docker.com/engine/security/#docker-daemon-attack-surface). - -## Configuration variable - -You can create configuration variables on the user, organization and repository level. -The level of the variable depends on where you created it. - -### Naming conventions - -The following rules apply to variable names: - -- Variable names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed. - -- Variable names must not start with the `GITHUB_` and `GITEA_` prefix. - -- Variable names must not start with a number. - -- Variable names are case-insensitive. - -- Variable names must be unique at the level they are created at. - -- Variable names must not be `CI`. - -### Using variable - -After creating configuration variables, they will be automatically filled in the `vars` context. -They can be accessed through expressions like `{{ vars.VARIABLE_NAME }}` in the workflow. - -### Precedence - -If a variable with the same name exists at multiple levels, the variable at the lowest level takes precedence: -A repository variable will always be chosen over an organization/user variable. diff --git a/docs/content/usage/actions/act-runner.zh-cn.md b/docs/content/usage/actions/act-runner.zh-cn.md index 274b0f0692..e5ebff976d 100644 --- a/docs/content/usage/actions/act-runner.zh-cn.md +++ b/docs/content/usage/actions/act-runner.zh-cn.md @@ -258,32 +258,3 @@ Runner的标签用于确定Runner可以运行哪些Job以及如何运行它们 Runner将从Gitea实例获取Job并自动运行它们。 由于Act Runner仍处于开发中,建议定期检查最新版本并进行升级。 - -## 变量 - -您可以创建用户、组织和仓库级别的变量。变量的级别取决于创建它的位置。 - -### 命名规则 - -以下规则适用于变量名: - -- 变量名称只能包含字母数字字符 (`[a-z]`, `[A-Z]`, `[0-9]`) 或下划线 (`_`)。不允许使用空格。 - -- 变量名称不能以 `GITHUB_` 和 `GITEA_` 前缀开头。 - -- 变量名称不能以数字开头。 - -- 变量名称不区分大小写。 - -- 变量名称在创建它们的级别上必须是唯一的。 - -- 变量名称不能为 “CI”。 - -### 使用 - -创建配置变量后,它们将自动填充到 `vars` 上下文中。您可以在工作流中使用类似 `{{ vars.VARIABLE_NAME }}` 这样的表达式来使用它们。 - -### 优先级 - -如果同名变量存在于多个级别,则级别最低的变量优先。 -仓库级别的变量总是比组织或者用户级别的变量优先被选中。 diff --git a/docs/content/usage/actions/comparison.zh-cn.md b/docs/content/usage/actions/comparison.zh-cn.md index dbe9ca007d..16b2181ba2 100644 --- a/docs/content/usage/actions/comparison.zh-cn.md +++ b/docs/content/usage/actions/comparison.zh-cn.md @@ -95,12 +95,6 @@ Gitea Actions目前不支持此功能,如果使用它,结果将始终为空 ## 缺失的功能 -### 变量 - -请参阅[变量](https://docs.github.com/zh/actions/learn-github-actions/variables)。 - -目前变量功能正在开发中。 - ### 问题匹配器 问题匹配器是一种扫描Actions输出以查找指定正则表达式模式并在用户界面中突出显示该信息的方法。 diff --git a/docs/content/usage/actions/design.en-us.md b/docs/content/usage/actions/design.en-us.md index 29fa433e59..0d72c19dce 100644 --- a/docs/content/usage/actions/design.en-us.md +++ b/docs/content/usage/actions/design.en-us.md @@ -104,7 +104,7 @@ However, if a job container tries to fetch code from localhost, it will fail bec ### Connection 3, act runner to internet When you use some actions like `actions/checkout@v4`, the act runner downloads the scripts, not the job containers. -By default, it downloads from [gitea.com](http://gitea.com/), so it requires access to the internet. +By default, it downloads from [github.com](http://github.com/), so it requires access to the internet. If you configure the `DEFAULT_ACTIONS_URL` to `self`, then it will download from your Gitea instance by default. Then it will not connect to internet when downloading the action itself. It also downloads some docker images from Docker Hub by default, which also requires internet access. However, internet access is not strictly necessary. diff --git a/docs/content/usage/actions/design.zh-cn.md b/docs/content/usage/actions/design.zh-cn.md index 8add1cf7c5..f48576477f 100644 --- a/docs/content/usage/actions/design.zh-cn.md +++ b/docs/content/usage/actions/design.zh-cn.md @@ -105,7 +105,8 @@ act runner 必须能够连接到Gitea以接收任务并发送执行结果回来 ### 连接 3,act runner到互联网 当您使用诸如 `actions/checkout@v4` 的一些Actions时,act runner下载的是脚本,而不是Job容器。 -默认情况下,它从[gitea.com](http://gitea.com/)下载,因此需要访问互联网。 +默认情况下,它从[github.com](http://github.com/)下载,因此需要访问互联网。如果您设置的是 self, +那么默认将从您的当前Gitea实例下载,那么此步骤不需要连接到互联网。 它还默认从Docker Hub下载一些Docker镜像,这也需要互联网访问。 然而,互联网访问并不是绝对必需的。 diff --git a/docs/content/usage/actions/faq.en-us.md b/docs/content/usage/actions/faq.en-us.md index 7ed59e02cd..427d57c43e 100644 --- a/docs/content/usage/actions/faq.en-us.md +++ b/docs/content/usage/actions/faq.en-us.md @@ -45,25 +45,24 @@ It is technically possible to implement, but we need to discuss whether it is ne ## Where will the runner download scripts when using actions such as `actions/checkout@v4`? -You may be aware that there are tens of thousands of [marketplace actions](https://github.com/marketplace?type=actions) in GitHub. -However, when you write `uses: actions/checkout@v4`, it actually downloads the scripts from [gitea.com/actions/checkout](http://gitea.com/actions/checkout) by default (not GitHub). -This is a mirror of [github.com/actions/checkout](http://github.com/actions/checkout), but it's impossible to mirror all of them. -That's why you may encounter failures when trying to use some actions that haven't been mirrored. +There are tens of thousands of [actions scripts](https://github.com/marketplace?type=actions) in GitHub, and when you write `uses: actions/checkout@v4`, it downloads the scripts from [github.com/actions/checkout](http://github.com/actions/checkout) by default. +But what if you want to use actions from other places such as gitea.com instead of GitHub? The good news is that you can specify the URL prefix to use actions from anywhere. This is an extra syntax in Gitea Actions. For example: -- `uses: https://github.com/xxx/xxx@xxx` - `uses: https://gitea.com/xxx/xxx@xxx` +- `uses: https://github.com/xxx/xxx@xxx` - `uses: http://your_gitea_instance.com/xxx@xxx` Be careful, the `https://` or `http://` prefix is necessary! -Alternatively, if you want your runners to download actions from GitHub or your own Gitea instance by default, you can configure it by setting `[actions].DEFAULT_ACTIONS_URL`. -See [Configuration Cheat Sheet](administration/config-cheat-sheet.md#actions-actions). +This is one of the differences from GitHub Actions which supports actions scripts only from GitHub. +But it should allow users much more flexibility in how they run Actions. -This is one of the differences from GitHub Actions, but it should allow users much more flexibility in how they run Actions. +Alternatively, if you want your runners to download actions from your own Gitea instance by default, you can configure it by setting `[actions].DEFAULT_ACTIONS_URL`. +See [Configuration Cheat Sheet](administration/config-cheat-sheet.md#actions-actions). ## How to limit the permission of the runners? diff --git a/docs/content/usage/actions/faq.zh-cn.md b/docs/content/usage/actions/faq.zh-cn.md index ba5f87bf0c..d6e1466801 100644 --- a/docs/content/usage/actions/faq.zh-cn.md +++ b/docs/content/usage/actions/faq.zh-cn.md @@ -45,25 +45,25 @@ DEFAULT_REPO_UNITS = ...,repo.actions ## 使用`actions/checkout@v4`等Actions时,Job容器会从何处下载脚本? -您可能知道GitHub上有成千上万个[Actions市场](https://github.com/marketplace?type=actions)。 -然而,当您编写`uses: actions/checkout@v4`时,它实际上默认从[gitea.com/actions/checkout](http://gitea.com/actions/checkout)下载脚本(而不是从GitHub下载)。 -这是[github.com/actions/checkout](http://github.com/actions/checkout)的镜像,但无法将它们全部镜像。 -这就是为什么在尝试使用尚未镜像的某些Actions时可能会遇到失败的原因。 +GitHub 上有成千上万个 [Actions 脚本](https://github.com/marketplace?type=actions)。 +当您编写 `uses: actions/checkout@v4` 时,它默认会从 [github.com/actions/checkout](https://github.com/actions/checkout) 下载脚本。 +那如果您想使用一些托管在其它平台上的脚本呢,比如在 gitea.com 上的? 好消息是,您可以指定要从任何位置使用Actions的URL前缀。 这是Gitea Actions中的额外语法。 例如: -- `uses: https://github.com/xxx/xxx@xxx` - `uses: https://gitea.com/xxx/xxx@xxx` +- `uses: https://github.com/xxx/xxx@xxx` - `uses: http://your_gitea_instance.com/xxx@xxx` 注意,`https://`或`http://`前缀是必需的! -另外,如果您希望您的Runner默认从GitHub或您自己的Gitea实例下载Actions,可以通过设置 `[actions].DEFAULT_ACTIONS_URL`进行配置。 -参见[配置速查表](administration/config-cheat-sheet.md#actions-actions)。 +这是与 GitHub Actions 的一个区别,GitHub Actions 只允许使用托管在 GitHub 上的 actions 脚本。 +但用户理应拥有权利去灵活决定如何运行 Actions。 -这是与GitHub Actions的一个区别,但它应该允许用户以更灵活的方式运行Actions。 +另外,如果您希望您的 Runner 默认从您自己的 Gitea 实例下载 Actions,可以通过设置 `[actions].DEFAULT_ACTIONS_URL`进行配置。 +参见[配置速查表](administration/config-cheat-sheet.md#actions-actions)。 ## 如何限制Runner的权限? diff --git a/docs/content/usage/actions/quickstart.en-us.md b/docs/content/usage/actions/quickstart.en-us.md index 2a2cf72584..0514b6ddf2 100644 --- a/docs/content/usage/actions/quickstart.en-us.md +++ b/docs/content/usage/actions/quickstart.en-us.md @@ -61,8 +61,8 @@ It is always a bad idea to use a loopback address such as `127.0.0.1` or `localh If you are unsure which address to use, the LAN address is usually the right choice. `token` is used for authentication and identification, such as `P2U1U0oB4XaRCi8azcngmPCLbRpUGapalhmddh23`. -It is one-time use only and cannot be used to register multiple runners. -You can obtain different levels of 'tokens' from the following places to create the corresponding level of' runners': +Each token can be used to create multiple runners, until it is replaced with a new token using the reset link. +You can obtain different levels of 'tokens' from the following places to create the corresponding level of 'runners': - Instance level: The admin settings page, like `<your_gitea.com>/admin/actions/runners`. - Organization level: The organization settings page, like `<your_gitea.com>/<org>/settings/actions/runners`. diff --git a/docs/content/usage/actions/variables.en-us.md b/docs/content/usage/actions/variables.en-us.md new file mode 100644 index 0000000000..dee2e74234 --- /dev/null +++ b/docs/content/usage/actions/variables.en-us.md @@ -0,0 +1,41 @@ +--- +date: "2024-04-10T22:21:00+08:00" +title: "Variables" +slug: "actions-variables" +sidebar_position: 25 +draft: false +toc: false +menu: + sidebar: + parent: "actions" + name: "Variables" + sidebar_position: 25 + identifier: "actions-variables" +--- + +## Variables + +You can create configuration variables on the user, organization and repository level. +The level of the variable depends on where you created it. When creating a variable, the +key will be converted to uppercase. You need use uppercase on the yaml file. + +### Naming conventions + +The following rules apply to variable names: + +- Variable names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed. +- Variable names must not start with the `GITHUB_` and `GITEA_` prefix. +- Variable names must not start with a number. +- Variable names are case-insensitive. +- Variable names must be unique at the level they are created at. +- Variable names must not be `CI`. + +### Using variable + +After creating configuration variables, they will be automatically filled in the `vars` context. +They can be accessed through expressions like `${{ vars.VARIABLE_NAME }}` in the workflow. + +### Precedence + +If a variable with the same name exists at multiple levels, the variable at the lowest level takes precedence: +A repository variable will always be chosen over an organization/user variable. diff --git a/docs/content/usage/actions/variables.zh-cn.md b/docs/content/usage/actions/variables.zh-cn.md new file mode 100644 index 0000000000..77643408a1 --- /dev/null +++ b/docs/content/usage/actions/variables.zh-cn.md @@ -0,0 +1,39 @@ +--- +date: "2024-04-10T22:21:00+08:00" +title: "变量" +slug: "actions-variables" +sidebar_position: 25 +draft: false +toc: false +menu: + sidebar: + parent: "actions" + name: "变量" + sidebar_position: 25 + identifier: "actions-variables" +--- + +## 变量 + +您可以创建用户、组织和仓库级别的变量。变量的级别取决于创建它的位置。当创建变量时,变量的名称会被 +转换为大写,在yaml文件中引用时需要使用大写。 + +### 命名规则 + +以下规则适用于变量名: + +- 变量名称只能包含字母数字字符 (`[a-z]`, `[A-Z]`, `[0-9]`) 或下划线 (`_`)。不允许使用空格。 +- 变量名称不能以 `GITHUB_` 和 `GITEA_` 前缀开头。 +- 变量名称不能以数字开头。 +- 变量名称不区分大小写。 +- 变量名称在创建它们的级别上必须是唯一的。 +- 变量名称不能为 `CI`。 + +### 使用 + +创建配置变量后,它们将自动填充到 `vars` 上下文中。您可以在工作流中使用类似 `${{ vars.VARIABLE_NAME }}` 这样的表达式来使用它们。 + +### 优先级 + +如果同名变量存在于多个级别,则级别最低的变量优先。 +仓库级别的变量总是比组织或者用户级别的变量优先被选中。 diff --git a/docs/content/usage/badge.en-us.md b/docs/content/usage/badge.en-us.md new file mode 100644 index 0000000000..212134e01c --- /dev/null +++ b/docs/content/usage/badge.en-us.md @@ -0,0 +1,37 @@ +--- +date: "2023-02-25T00:00:00+00:00" +title: "Badge" +slug: "badge" +sidebar_position: 11 +toc: false +draft: false +aliases: + - /en-us/badge +menu: + sidebar: + parent: "usage" + name: "Badge" + sidebar_position: 11 + identifier: "Badge" +--- + +# Badge + +Gitea has its builtin Badge system which allows you to display the status of your repository in other places. You can use the following badges: + +## Workflow Badge + +The Gitea Actions workflow badge is a badge that shows the status of the latest workflow run. +It is designed to be compatible with [GitHub Actions workflow badge](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge). + +You can use the following URL to get the badge: + +``` +https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event} +``` + +- `{owner}`: The owner of the repository. +- `{repo}`: The name of the repository. +- `{workflow_file}`: The name of the workflow file. +- `{branch}`: Optional. The branch of the workflow. Default to your repository's default branch. +- `{event}`: Optional. The event of the workflow. Default to none. diff --git a/docs/content/usage/blocking-users.en-us.md b/docs/content/usage/blocking-users.en-us.md new file mode 100644 index 0000000000..b59bbe4d62 --- /dev/null +++ b/docs/content/usage/blocking-users.en-us.md @@ -0,0 +1,56 @@ +--- +date: "2024-01-31T00:00:00+00:00" +title: "Blocking a user" +slug: "blocking-user" +sidebar_position: 25 +toc: false +draft: false +aliases: + - /en-us/webhooks +menu: + sidebar: + parent: "usage" + name: "Blocking a user" + sidebar_position: 30 + identifier: "blocking-user" +--- + +# Blocking a user + +Gitea supports blocking of users to restrict how they can interact with you and your content. + +You can block a user in your account settings, from the user's profile or from comments created by the user. +The user is not directly notified about the block, but they can notice they are blocked when they attempt to interact with you. +Organization owners can block anyone who is not a member of the organization too. +If a blocked user has admin permissions, they can still perform all actions even if blocked. + +### When you block a user + +- the user stops following you +- you stop following the user +- the user's stars are removed from your repositories +- your stars are removed from their repositories +- the user stops watching your repositories +- you stop watching their repositories +- the user's issue assignments are removed from your repositories +- your issue assignments are removed from their repositories +- the user is removed as a collaborator on your repositories +- you are removed as a collaborator on their repositories +- any pending repository transfers to or from the blocked user are canceled + +### When you block a user, the user cannot + +- follow you +- watch your repositories +- star your repositories +- fork your repositories +- transfer repositories to you +- open issues or pull requests on your repositories +- comment on issues or pull requests you've created +- comment on issues or pull requests on your repositories +- react to your comments on issues or pull requests +- react to comments on issues or pull requests on your repositories +- assign you to issues or pull requests +- add you as a collaborator on their repositories +- send you notifications by @mentioning your username +- be added as team member (if blocked by an organization) diff --git a/docs/content/usage/issue-pull-request-templates.en-us.md b/docs/content/usage/issue-pull-request-templates.en-us.md index 34475e3465..e203c0d379 100644 --- a/docs/content/usage/issue-pull-request-templates.en-us.md +++ b/docs/content/usage/issue-pull-request-templates.en-us.md @@ -19,9 +19,10 @@ menu: Some projects have a standard list of questions that users need to answer when creating an issue or pull request. Gitea supports adding templates to the -main branch of the repository so that they can autopopulate the form when users are +**default branch of the repository** so that they can autopopulate the form when users are creating issues and pull requests. This will cut down on the initial back and forth of getting some clarifying details. +It is currently not possible to provide generic issue/pull-request templates globally. Additionally, the New Issue page URL can be suffixed with `?title=Issue+Title&body=Issue+Text` and the form will be populated with those strings. Those strings will be used instead of the template if there is one. @@ -135,6 +136,12 @@ body: attributes: value: | Thanks for taking the time to fill out this bug report! + # some markdown that will only be visible once the issue has been created + - type: markdown + attributes: + value: | + This issue was created by an issue **template** :) + visible: [content] - type: input id: contact attributes: @@ -186,11 +193,16 @@ body: options: - label: I agree to follow this project's Code of Conduct required: true + - label: I have also read the CONTRIBUTION.MD + required: true + visible: [form] + - label: This is a TODO only visible after issue creation + visible: [content] ``` ### Markdown -You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted. +You can use a `markdown` element to display Markdown in your form that provides extra context to the user, but is not submitted by default. Attributes: @@ -198,6 +210,8 @@ Attributes: |-------|--------------------------------------------------------------|----------|--------|---------|--------------| | value | The text that is rendered. Markdown formatting is supported. | Required | String | - | - | +visible: Default is **[form]** + ### Textarea You can use a `textarea` element to add a multi-line text field to your form. Contributors can also attach files in `textarea` fields. @@ -218,6 +232,8 @@ Validations: |----------|------------------------------------------------------|----------|---------|---------|--------------| | required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +visible: Default is **[form, content]** + ### Input You can use an `input` element to add a single-line text field to your form. @@ -239,6 +255,8 @@ Validations: | is_number | Prevents form submission until element is filled with a number. | Optional | Boolean | false | - | | regex | Prevents form submission until element is filled with a value that match the regular expression. | Optional | String | - | a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) | +visible: Default is **[form, content]** + ### Dropdown You can use a `dropdown` element to add a dropdown menu in your form. @@ -258,6 +276,8 @@ Validations: |----------|------------------------------------------------------|----------|---------|---------|--------------| | required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +visible: Default is **[form, content]** + ### Checkboxes You can use the `checkboxes` element to add a set of checkboxes to your form. @@ -265,17 +285,20 @@ You can use the `checkboxes` element to add a set of checkboxes to your form. Attributes: | Key | Description | Required | Type | Default | Valid values | -|-------------|-------------------------------------------------------------------------------------------------------|----------|--------|--------------|--------------| +| ----------- | ----------------------------------------------------------------------------------------------------- | -------- | ------ | ------------ | ------------ | | label | A brief description of the expected user input, which is displayed in the form. | Required | String | - | - | | description | A description of the set of checkboxes, which is displayed in the form. Supports Markdown formatting. | Optional | String | Empty String | - | | options | An array of checkboxes that the user can select. For syntax, see below. | Required | Array | - | - | For each value in the options array, you can set the following keys. -| Key | Description | Required | Type | Default | Options | -|----------|------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|---------|---------| -| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - | -| required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +| Key | Description | Required | Type | Default | Options | +|--------------|------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------|---------|---------| +| label | The identifier for the option, which is displayed in the form. Markdown is supported for bold or italic text formatting, and hyperlinks. | Required | String | - | - | +| required | Prevents form submission until element is completed. | Optional | Boolean | false | - | +| visible | Whether a specific checkbox appears in the form only, in the created issue only, or both. Valid options are "form" and "content". | Optional | String array | false | - | + +visible: Default is **[form, content]** ## Syntax for issue config @@ -291,15 +314,15 @@ contact_links: ### Possible Options -| Key | Description | Type | Default | -|----------------------|-------------------------------------------------------------------------------------------------------|--------------------|----------------| -| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true | -| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array | +| Key | Description | Type | Default | +|----------------------|-------------------------------------------------------|--------------------|-------------| +| blank_issues_enabled | If set to false, the User is forced to use a Template | Boolean | true | +| contact_links | Custom Links to show in the Choose Box | Contact Link Array | Empty Array | ### Contact Link -| Key | Description | Type | Required | -|----------------------|-------------------------------------------------------------------------------------------------------|---------|----------| -| name | the name of your link | String | true | -| url | The URL of your Link | String | true | -| about | A short description of your Link | String | true | +| Key | Description | Type | Required | +|-------|----------------------------------|--------|----------| +| name | the name of your link | String | true | +| url | The URL of your Link | String | true | +| about | A short description of your Link | String | true | diff --git a/docs/content/usage/packages/container.en-us.md b/docs/content/usage/packages/container.en-us.md index 6be21c2b27..5676aa36fb 100644 --- a/docs/content/usage/packages/container.en-us.md +++ b/docs/content/usage/packages/container.en-us.md @@ -39,6 +39,16 @@ Images must follow this naming convention: `{registry}/{owner}/{image}` +When building your docker image, using the naming convention above, this looks like: + +```shell +# build an image with tag +docker build -t {registry}/{owner}/{image}:{tag} . +# name an existing image with tag +docker tag {some-existing-image}:{tag} {registry}/{owner}/{image}:{tag} +``` + +where your registry is the domain of your gitea instance (e.g. gitea.example.com). For example, these are all valid image names for the owner `testuser`: `gitea.example.com/testuser/myimage` diff --git a/docs/content/usage/packages/overview.en-us.md b/docs/content/usage/packages/overview.en-us.md index 44d18ff482..89fc6f286e 100644 --- a/docs/content/usage/packages/overview.en-us.md +++ b/docs/content/usage/packages/overview.en-us.md @@ -42,7 +42,7 @@ The following package managers are currently supported: | [PyPI](usage/packages/pypi.md) | Python | `pip`, `twine` | | [RPM](usage/packages/rpm.md) | - | `yum`, `dnf`, `zypper` | | [RubyGems](usage/packages/rubygems.md) | Ruby | `gem`, `Bundler` | -| [Swift](usage/packages/rubygems.md) | Swift | `swift` | +| [Swift](usage/packages/swift.md) | Swift | `swift` | | [Vagrant](usage/packages/vagrant.md) | - | `vagrant` | **The following paragraphs only apply if Packages are not globally disabled!** diff --git a/docs/content/usage/packages/swift.en-us.md b/docs/content/usage/packages/swift.en-us.md index 606fa20b36..38eb155641 100644 --- a/docs/content/usage/packages/swift.en-us.md +++ b/docs/content/usage/packages/swift.en-us.md @@ -26,7 +26,8 @@ To work with the Swift package registry, you need to use [swift](https://www.swi To register the package registry and provide credentials, execute: ```shell -swift package-registry set https://gitea.example.com/api/packages/{owner}/swift -login {username} -password {password} +swift package-registry set https://gitea.example.com/api/packages/{owner}/swift +swift package-registry login https://gitea.example.com/api/packages/{owner}/swift --username {username} --password {password} ``` | Placeholder | Description | diff --git a/docs/content/usage/profile-readme.zh-cn.md b/docs/content/usage/profile-readme.zh-cn.md index 804f69d2e6..b69d4aa921 100644 --- a/docs/content/usage/profile-readme.zh-cn.md +++ b/docs/content/usage/profile-readme.zh-cn.md @@ -15,6 +15,10 @@ menu: # 个人资料 README -要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 ".profile" 的仓库,并编辑其中的 README.md 文件。Gitea 将自动获取该文件并在您的仓库上方显示。 +要在您的 Gitea 个人资料页面显示一个 Markdown 文件,只需创建一个名为 `.profile` 的仓库,并编辑其中的 `README.md` 文件。Gitea 将自动获取该文件并在您的仓库上方显示。 注意:您可以将此仓库设为私有。这样可以隐藏您的源文件,使其对公众不可见,并允许您将某些文件设为私有。但是,README.md 文件将是您个人资料上唯一存在的文件。如果您希望完全私有化 .profile 仓库,则需删除或重命名 README.md 文件。 + +用户示例 `.profile/README.md`: + + diff --git a/docs/scripts/trans-copy.sh b/docs/scripts/trans-copy.sh deleted file mode 100755 index 7374ab9e73..0000000000 --- a/docs/scripts/trans-copy.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -e - -# -# This script is used to copy the en-US content to our available locales as a -# fallback to always show all pages when displaying a specific locale that is -# missing some documents to be translated. -# -# Just execute the script without any argument and you will get the missing -# files copied into the content folder. We are calling this script within the CI -# server simply by `make trans-copy`. -# - -declare -a LOCALES=( - "fr-fr" - "nl-nl" - "pt-br" - "zh-cn" - "zh-tw" -) - -ROOT=$(realpath $(dirname $0)/..) - -for SOURCE in $(find ${ROOT}/content -type f -iname *.en-us.md); do - for LOCALE in "${LOCALES[@]}"; do - DEST="${SOURCE%.en-us.md}.${LOCALE}.md" - - if [[ ! -f ${DEST} ]]; then - cp ${SOURCE} ${DEST} - sed -i.bak "s/en\-us/${LOCALE}/g" ${DEST} - rm ${DEST}.bak - fi - done -done diff --git a/go.mod b/go.mod index 7a752ec874..1e0f1ea8f8 100644 --- a/go.mod +++ b/go.mod @@ -1,143 +1,143 @@ module code.gitea.io/gitea -go 1.21 +go 1.22 require ( - code.gitea.io/actions-proto-go v0.3.1 + code.gitea.io/actions-proto-go v0.4.0 code.gitea.io/gitea-vet v0.2.3 code.gitea.io/sdk/gitea v0.17.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 - gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 + connectrpc.com/connect v1.15.0 + gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028 gitea.com/go-chi/cache v0.2.0 - gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 - gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 + gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 + gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/NYTimes/gziphandler v1.1.1 - github.com/PuerkitoBio/goquery v1.8.1 - github.com/alecthomas/chroma/v2 v2.12.0 + github.com/PuerkitoBio/goquery v1.9.1 + github.com/alecthomas/chroma/v2 v2.13.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.3.10 - github.com/bufbuild/connect-go v1.10.0 - github.com/buildkite/terminal-to-html/v3 v3.10.1 + github.com/buildkite/terminal-to-html/v3 v3.11.0 github.com/caddyserver/certmagic v0.20.0 github.com/chi-middleware/proxy v1.1.1 - github.com/denisenkom/go-mssqldb v0.12.3 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 github.com/djherbis/buffer v1.2.0 github.com/djherbis/nio/v3 v3.0.1 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 github.com/dustin/go-humanize v1.0.1 - github.com/editorconfig/editorconfig-core-go/v2 v2.6.0 + github.com/editorconfig/editorconfig-core-go/v2 v2.6.1 github.com/emersion/go-imap v1.2.1 github.com/emirpasic/gods v1.18.1 github.com/ethantkoenig/rupture v1.0.1 - github.com/felixge/fgprof v0.9.3 + github.com/felixge/fgprof v0.9.4 github.com/fsnotify/fsnotify v1.7.0 github.com/gliderlabs/ssh v0.3.6 - github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 + github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.0.11 + github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 - github.com/go-enry/go-enry/v2 v2.8.6 + github.com/go-enry/go-enry/v2 v2.8.7 github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.11.0 github.com/go-ldap/ldap/v3 v3.4.6 - github.com/go-sql-driver/mysql v1.7.1 + github.com/go-sql-driver/mysql v1.8.0 github.com/go-swagger/go-swagger v0.30.5 - github.com/go-testfixtures/testfixtures/v3 v3.9.0 - github.com/go-webauthn/webauthn v0.10.0 + github.com/go-testfixtures/testfixtures/v3 v3.10.0 + github.com/go-webauthn/webauthn v0.10.2 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 - github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-github/v57 v57.0.0 - github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 - github.com/google/uuid v1.5.0 + github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 + github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.1.2 github.com/gorilla/sessions v1.2.2 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.4.0 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 - github.com/jhillyerd/enmime v1.1.0 + github.com/jhillyerd/enmime v1.2.0 github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 - github.com/klauspost/compress v1.17.4 - github.com/klauspost/cpuid/v2 v2.2.6 + github.com/klauspost/compress v1.17.7 + github.com/klauspost/cpuid/v2 v2.2.7 github.com/lib/pq v1.10.9 - github.com/markbates/goth v1.78.0 + github.com/markbates/goth v1.79.0 github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-sqlite3 v1.14.19 - github.com/meilisearch/meilisearch-go v0.26.1 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/meilisearch/meilisearch-go v0.26.2 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.26 - github.com/minio/minio-go/v7 v7.0.66 - github.com/minio/sha256-simd v1.0.1 + github.com/microsoft/go-mssqldb v1.7.0 + github.com/minio/minio-go/v7 v7.0.69 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.7.0 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0-rc6 + github.com/opencontainers/image-spec v1.1.0 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 - github.com/prometheus/client_golang v1.18.0 + github.com/prometheus/client_golang v1.19.0 github.com/quasoft/websspi v1.1.2 - github.com/redis/go-redis/v9 v9.4.0 + github.com/redis/go-redis/v9 v9.5.1 github.com/robfig/cron/v3 v3.0.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 - github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd + github.com/sassoftware/go-rpmutils v0.3.0 github.com/sergi/go-diff v1.3.1 github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.11 github.com/urfave/cli/v2 v2.27.1 - github.com/xanzy/go-gitlab v0.96.0 + github.com/xanzy/go-gitlab v0.100.0 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 - github.com/yuin/goldmark v1.6.0 + github.com/yuin/goldmark v1.7.0 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.22.0 golang.org/x/image v0.15.0 - golang.org/x/net v0.20.0 - golang.org/x/oauth2 v0.16.0 - golang.org/x/sys v0.16.0 + golang.org/x/net v0.24.0 + golang.org/x/oauth2 v0.18.0 + golang.org/x/sys v0.19.0 golang.org/x/text v0.14.0 - golang.org/x/tools v0.17.0 - google.golang.org/grpc v1.60.1 - google.golang.org/protobuf v1.32.0 + golang.org/x/tools v0.19.0 + google.golang.org/grpc v1.62.1 + google.golang.org/protobuf v1.33.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/xurls/v2 v2.5.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.13 - xorm.io/xorm v1.3.7 + xorm.io/xorm v1.3.8 ) require ( - cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute v1.25.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect dario.cat/mergo v1.0.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect - github.com/ClickHouse/ch-go v0.61.1 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.17.1 // indirect + github.com/ClickHouse/ch-go v0.61.5 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.22.0 // indirect github.com/DataDog/zstd v1.5.5 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/RoaringBitmap/roaring v1.7.0 // indirect + github.com/RoaringBitmap/roaring v1.9.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect @@ -145,12 +145,12 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect - github.com/blevesearch/bleve_index_api v1.1.5 // indirect - github.com/blevesearch/geo v0.1.19 // indirect + github.com/blevesearch/bleve_index_api v1.1.6 // indirect + github.com/blevesearch/geo v0.1.20 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.2.6 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.2.8 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect @@ -166,43 +166,43 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect - github.com/couchbase/gomemcached v0.3.0 // indirect + github.com/couchbase/gomemcached v0.3.1 // indirect github.com/couchbase/goutils v0.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.5.0 // indirect - github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-openapi/analysis v0.22.2 // indirect - github.com/go-openapi/errors v0.21.0 // indirect - github.com/go-openapi/inflect v0.19.0 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/loads v0.21.5 // indirect - github.com/go-openapi/runtime v0.26.2 // indirect - github.com/go-openapi/spec v0.20.14 // indirect - github.com/go-openapi/strfmt v0.22.0 // indirect - github.com/go-openapi/swag v0.22.7 // indirect - github.com/go-openapi/validate v0.22.6 // indirect - github.com/go-webauthn/x v0.1.6 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/inflect v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/runtime v0.28.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-webauthn/x v0.1.9 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.0 // indirect @@ -230,6 +230,7 @@ require ( github.com/mholt/acmez v1.2.0 // indirect github.com/miekg/dns v1.1.58 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -241,16 +242,16 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/paulmach/orb v0.11.0 // indirect + github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.46.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/rhysd/actionlint v1.6.26 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.50.0 // indirect + github.com/prometheus/procfs v0.13.0 // indirect + github.com/rhysd/actionlint v1.6.27 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -260,7 +261,7 @@ require ( github.com/shopspring/decimal v1.3.1 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.2.1 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect @@ -271,28 +272,28 @@ require ( github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/zeebo/blake3 v0.2.3 // indirect - go.etcd.io/bbolt v1.3.8 // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect - go.opentelemetry.io/otel v1.22.0 // indirect - go.opentelemetry.io/otel/trace v1.22.0 // indirect + go.etcd.io/bbolt v1.3.9 // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/mod v0.14.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect + golang.org/x/mod v0.16.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -302,7 +303,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 -replace github.com/nektos/act => gitea.com/gitea/act v0.2.51 +replace github.com/nektos/act => gitea.com/gitea/act v0.259.1 replace github.com/gorilla/feeds => github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5 diff --git a/go.sum b/go.sum index b3b8ad8ce4..864bed6677 100644 --- a/go.sum +++ b/go.sum @@ -1,64 +1,33 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= +cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -code.gitea.io/actions-proto-go v0.3.1 h1:PMyiQtBKb8dNnpEO2R5rcZdXSis+UQZVo/SciMtR1aU= -code.gitea.io/actions-proto-go v0.3.1/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= +code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zCwnsU= +code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8= code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM= +connectrpc.com/connect v1.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo= +connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/gitea/act v0.2.51 h1:gXc/B4OlTciTTzAx9cmNyw04n2SDO7exPjAsR5Idu+c= -gitea.com/gitea/act v0.2.51/go.mod h1:CoaX2053jqBlD6JMgu4d4UgFL/rp2I14Kt5mMqcs0Z0= -gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo= -gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= -gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0= +gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw= +gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8= +gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028 h1:6/QAx4+s0dyRwdaTFPTnhGppuiuu0OqxIH9szyTpvKw= +gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w= gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE= -gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 h1:klh0LjhH7l4CuJkxlCM//o3rWLvWqxUpFxEtoYg5TNY= -gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384/go.mod h1:hQ9SYHKdOX968wJglb/NMQ+UqpOKwW4L+EYdvkWjHSo= -gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 h1:4FuO+MahrkDjdjVIS8ExmY9FEHTZS8TPheEm4uU5xLI= -gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3/go.mod h1:fc/pjt5EqNKgqQXYzcas1Z5L5whkZHyOvTA7OzWVJck= +gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo= +gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk= +gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q= +gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= @@ -69,18 +38,25 @@ github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 h1:r3qt8PCHnfjOv9PN3H github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121/go.mod h1:Ock8XgA7pvULhIaHGAk/cDnRfNrF9Jey81nPcc403iU= github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U= github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/ch-go v0.61.1 h1:j5rx3qnvcnYjhnP1IdXE/vdIRQiqgwAzyqOaasA6QCw= -github.com/ClickHouse/ch-go v0.61.1/go.mod h1:myxt/JZgy2BYHFGQqzmaIpbfr5CMbs3YHVULaWQj5YU= -github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4= -github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ= -github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= +github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= +github.com/ClickHouse/clickhouse-go/v2 v2.22.0 h1:LAdk0qT125PpSPnYepFQs5X5z1EwpAtIX10SUELPgi0= +github.com/ClickHouse/clickhouse-go/v2 v2.22.0/go.mod h1:tBhdF3f3RdP7sS59+oBAtTyhWpy0024ZxDMhgxra0QE= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= @@ -98,27 +74,26 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= -github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= +github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I= -github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68= -github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= -github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= -github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/RoaringBitmap/roaring v1.9.0 h1:lwKhr90/j0jVXJyh5X+vQN1VVn77rQFfYnh6RDRGCcE= +github.com/RoaringBitmap/roaring v1.9.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw= -github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw= +github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= +github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -143,10 +118,10 @@ github.com/blevesearch/bleve/v2 v2.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6 github.com/blevesearch/bleve/v2 v2.3.10 h1:z8V0wwGoL4rp7nG/O3qVVLYxUqCbEwskMt4iRJsPLgg= github.com/blevesearch/bleve/v2 v2.3.10/go.mod h1:RJzeoeHC+vNHsoLR54+crS1HmOWpnH87fL70HAUCzIA= github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= -github.com/blevesearch/bleve_index_api v1.1.5 h1:0q05mzu6GT/kebzqKywCpou/eUea9wTKa7kfqX7QX+k= -github.com/blevesearch/bleve_index_api v1.1.5/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= -github.com/blevesearch/geo v0.1.19 h1:hlX1YpBZ+X+xfjS8hEpmM/tdPUFbqBME3mdAWKHo2s0= -github.com/blevesearch/geo v0.1.19/go.mod h1:EPyr3iJCcESYa830PnkFhqzJkOP7/daHT/ocun43WRY= +github.com/blevesearch/bleve_index_api v1.1.6 h1:orkqDFCBuNU2oHW9hN2YEJmet+TE9orml3FCGbl1cKk= +github.com/blevesearch/bleve_index_api v1.1.6/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= +github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= +github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= @@ -155,8 +130,8 @@ github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+ github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= github.com/blevesearch/scorch_segment_api/v2 v2.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU= -github.com/blevesearch/scorch_segment_api/v2 v2.2.6 h1:rewrzgFaCEjjfWovAB9NubMAd4+aCLxD3RaQcPDaoNo= -github.com/blevesearch/scorch_segment_api/v2 v2.2.6/go.mod h1:0rv+k/OIjtYCT/g7Z45pCOVweFyta+0AdXO8keKfZxo= +github.com/blevesearch/scorch_segment_api/v2 v2.2.8 h1:+OLW38LuRKio6N6V0gIk1srwFz79FJ5v2sNqHz2HVAA= +github.com/blevesearch/scorch_segment_api/v2 v2.2.8/go.mod h1:ckbeb7knyOOvAdZinn/ASbB7EA3HoagnJkmEV3J7+sg= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= @@ -194,40 +169,34 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg= -github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= -github.com/buildkite/terminal-to-html/v3 v3.10.1 h1:znT9eD26LQ59dDJJEpMCwkP4wEptEAPi74hsTBuHdEo= -github.com/buildkite/terminal-to-html/v3 v3.10.1/go.mod h1:qtuRyYs6/Sw3FS9jUyVEaANHgHGqZsGqMknPLyau5cQ= +github.com/buildkite/terminal-to-html/v3 v3.11.0 h1:wMTpKgR61lqmxMz1FKjCaW5mq6DqeEgFZdJ+SU4hP30= +github.com/buildkite/terminal-to-html/v3 v3.11.0/go.mod h1:8JACDet3vmvWLsL4IBobweQYtf19W5J+EKM3LEE1c+4= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ= github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= -github.com/couchbase/go-couchbase v0.0.0-20201026062457-7b3be89bbd89/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk= github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= -github.com/couchbase/gomemcached v0.1.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= -github.com/couchbase/gomemcached v0.3.0 h1:XkMDdP6w7rtvLijDE0/RhcccX+XvAk5cboyBv1YcI0U= -github.com/couchbase/gomemcached v0.3.0/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= -github.com/couchbase/goutils v0.0.0-20201030094643-5e82bb967e67/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/couchbase/gomemcached v0.3.1 h1:jfspNuQIXgWy+5GUPQrsQ6yC5uJCfMmd/JKvK6C26r8= +github.com/couchbase/gomemcached v0.3.1/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= @@ -244,8 +213,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -260,17 +227,16 @@ github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmW github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.0 h1:5O8paxMLmi/5ONoKXzWNYxoSZU7+ITVbGcPga0IrzfE= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.0/go.mod h1:hdTKe+hwa3mMnMn4JUQziT+yc3pF+6EVmK2LPbLZthE= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.1 h1:iPCqofzMO41WVbcS/B5Ym7AwHQg9cyQ7Ie/R2XU5L3A= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.1/go.mod h1:VY4oyqUnpULFB3SCRpl24GFDIN1PmfiQIvN/G4ScSNg= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= @@ -284,16 +250,12 @@ github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTe github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ethantkoenig/rupture v1.0.1 h1:6aAXghmvtnngMgQzy7SMGdicMvkV86V4n9fT0meE5E4= github.com/ethantkoenig/rupture v1.0.1/go.mod h1:Sjqo/nbffZp1pVVXNGhpugIjsWmuS9KiIB4GtpEBur4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= +github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -304,30 +266,29 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= -github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8= github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= -github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 h1:j2TrkUG/NATGi/EQS+MvEoF79CxiRUmT16ErFroNcKI= -github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI= -github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 h1:I2nuhyVI/48VXoRCCZR2hYBgnSXa+EuDJf/VyX06TC0= -github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI= +github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225 h1:OoM81OclgRX7CUch4M7MmsH0NcmLWpFiSn7rhs6Y5ZU= +github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225/go.mod h1:yRUfFCoZY6C1CWalauqEQ5xYgSckzEBEO/2MBC6BOME= +github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 h1:H9MGShwybHLSln6K8RxHPMHiLcD86Lru+5TVW2TcXHY= +github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI= github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw= github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= -github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= -github.com/go-enry/go-enry/v2 v2.8.6 h1:T6ljs5+qNiUTDqpfK5GUD5EvLNdDbf804u8iC30vw7U= -github.com/go-enry/go-enry/v2 v2.8.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= +github.com/go-enry/go-enry/v2 v2.8.7 h1:vbab0pcf5Yo1cHQLzbWZ+QomUh3EfEU8EiR5n7W0lnQ= +github.com/go-enry/go-enry/v2 v2.8.7/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= @@ -345,38 +306,34 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= -github.com/go-openapi/analysis v0.22.2 h1:ZBmNoP2h5omLKr/srIC9bfqrUGzT6g6gNv03HE9Vpj0= -github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo= -github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY= -github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho= -github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= -github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/loads v0.21.5 h1:jDzF4dSoHw6ZFADCGltDb2lE4F6De7aWSpe+IcsRzT0= -github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8= -github.com/go-openapi/runtime v0.26.2 h1:elWyB9MacRzvIVgAZCBJmqTi7hBzU0hlKD4IvfX0Zl0= -github.com/go-openapi/runtime v0.26.2/go.mod h1:O034jyRZ557uJKzngbMDJXkcKJVzXJiymdSfgejrcRw= -github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= -github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI= -github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4= -github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8= -github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= -github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo= -github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= +github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4= +github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-swagger/go-swagger v0.30.5 h1:SQ2+xSonWjjoEMOV5tcOnZJVlfyUfCBhGQGArS1b9+U= github.com/go-swagger/go-swagger v0.30.5/go.mod h1:cWUhSyCNqV7J1wkkxfr5QmbcnCewetCdvEXqgPvbc/Q= github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0= @@ -384,16 +341,17 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.m github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY= -github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s= -github.com/go-webauthn/webauthn v0.10.0 h1:yuW2e1tXnRAwAvKrR4q4LQmc6XtCMH639/ypZGhZCwk= -github.com/go-webauthn/webauthn v0.10.0/go.mod h1:l0NiauXhL6usIKqNLCUM3Qir43GK7ORg8ggold0Uv/Y= -github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ= -github.com/go-webauthn/x v0.1.6/go.mod h1:W8dFVZ79o4f+nY1eOUICy/uq5dhrRl7mxQkYhXTo0FA= +github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc= +github.com/go-testfixtures/testfixtures/v3 v3.10.0/go.mod h1:z8RoleoNtibi6Ar8ziCW7e6PQ+jWiqbUWvuv8AMe4lo= +github.com/go-webauthn/webauthn v0.10.2 h1:OG7B+DyuTytrEPFmTX503K77fqs3HDK/0Iv+z8UYbq4= +github.com/go-webauthn/webauthn v0.10.2/go.mod h1:Gd1IDsGAybuvK1NkwUTLbGmeksxuRJjVN2PE/xsPxHs= +github.com/go-webauthn/x v0.1.9 h1:v1oeLmoaa+gPOaZqUdDentu6Rl7HkSSsmOT6gxEQHhE= +github.com/go-webauthn/x v0.1.9/go.mod h1:pJNMlIMP1SU7cN8HNlKJpLEnFHCygLCvaLZ8a1xeoQA= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -401,64 +359,40 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -471,27 +405,13 @@ github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 h1:WzfWbQz/Ze8v6l++GGbGNFZnUShVpP/0xffCPLL+ax8= -github.com/google/pprof v0.0.0-20240117000934-35fc243c5815/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= @@ -502,7 +422,6 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY= @@ -510,7 +429,6 @@ github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= @@ -521,8 +439,6 @@ github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -533,8 +449,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -555,23 +470,20 @@ github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= -github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jhillyerd/enmime v1.1.0 h1:ubaIzg68VY7CMCe2YbHe6nkRvU9vujixTkNz3EBvZOw= -github.com/jhillyerd/enmime v1.1.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I= +github.com/jhillyerd/enmime v1.2.0 h1:dIu1IPEymQgoT2dzuB//ttA/xcV40NMPpQtmd4wslHk= +github.com/jhillyerd/enmime v1.2.0/go.mod h1:FRFuUPCLh8PByQv+8xRcLO9QHqaqTqreYhopv5eyk4I= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -585,17 +497,16 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= @@ -609,12 +520,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= -github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= -github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= -github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -629,11 +537,10 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA= github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE= github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o= -github.com/markbates/goth v1.78.0 h1:7VEIFDycJp9deyVv3YraGBPdD0ZYQW93Y3Aw1eVP3BY= -github.com/markbates/goth v1.78.0/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc= +github.com/markbates/goth v1.79.0 h1:fUYi9R6VubVEK2bpmXvIUp7xRcxA68i8ovfUQx/i5Qc= +github.com/markbates/goth v1.79.0/go.mod h1:RBD+tcFnXul2NnYuODhnIweOcuVPkBohLfEvutPekcU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -643,22 +550,24 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A= -github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyqQ1hNveVsU2OE= +github.com/meilisearch/meilisearch-go v0.26.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs= +github.com/microsoft/go-mssqldb v1.7.0/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= -github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= +github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0= +github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -676,9 +585,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= @@ -704,22 +611,21 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU= -github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/paulmach/orb v0.11.0 h1:JfVXJUBeH9ifc/OrhBY0lL16QsmPgpCHMlqSSYhcgAA= -github.com/paulmach/orb v0.11.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= @@ -731,7 +637,8 @@ github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -740,31 +647,29 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= +github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw= github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= -github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rhysd/actionlint v1.6.26 h1:zi7jPZf3Ks14gCXYAAL47uBziyFlX7+Xwilqhexct9g= -github.com/rhysd/actionlint v1.6.26/go.mod h1:TIj1DlCgtYLOv5CH9wCK+WJTOr1qAdnFzkGi0IgSCO4= +github.com/rhysd/actionlint v1.6.27 h1:xxwe8YmveBcC8lydW6GoHMGmB6H/MTqUU60F2p10wjw= +github.com/rhysd/actionlint v1.6.27/go.mod h1:m2nFUjAnOrxCMXuOMz9evYBRCLUsMnKY2IJl/N5umbk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -781,8 +686,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd h1:KpbqRPDwcAQTyaP+L+YudTRb3CnJlQ64Hfn1SF/zHBA= -github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI= +github.com/sassoftware/go-rpmutils v0.3.0 h1:tE4TZ8KcOXay5iIP64P291s6Qxd9MQCYhI7DU+f3gFA= +github.com/sassoftware/go-rpmutils v0.3.0/go.mod h1:hM9wdxFsjUFR/tJ6SMsLrJuChcucCa0DsCzE9RMfwMo= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= @@ -800,8 +705,8 @@ github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= @@ -843,8 +748,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= @@ -868,23 +774,21 @@ github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6S github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.37.1-0.20220607072126-8a320890c08d/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= -github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= -github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/go-gitlab v0.96.0 h1:LGkZ+wSNMRtHIBaYE4Hq3dZVjprwHv3Y1+rhKU3WETs= -github.com/xanzy/go-gitlab v0.96.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= +github.com/xanzy/go-gitlab v0.100.0 h1:jaOtYj5nWI19+9oVVmgy233pax2oYqucwetogYU46ks= +github.com/xanzy/go-gitlab v0.100.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -895,8 +799,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5 h1:3seWKGVhGoc66Ht5QlhQsr4xT2caDnFegsnh2NqvENU= github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= @@ -904,12 +808,11 @@ github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBz github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= -github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= +github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= @@ -921,39 +824,29 @@ github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvv github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= -go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= +go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= -go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= -go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= -go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= -go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= -go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= -go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -963,80 +856,31 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw= +golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -1046,23 +890,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1070,43 +904,21 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1114,9 +926,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1129,9 +941,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1141,11 +952,9 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1158,158 +967,44 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1322,7 +1017,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -1337,16 +1031,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= @@ -1369,12 +1055,9 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs= strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/xorm v1.3.7 h1:mLceAGu0b87r9pD4qXyxGHxifOXIIrAdVcA6k95/osw= -xorm.io/xorm v1.3.7/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw= +xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo= +xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw= diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 5390f6288f..3d0a288e62 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -26,6 +26,8 @@ const ( ArtifactStatusUploadConfirmed // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed ArtifactStatusUploadError // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired + ArtifactStatusPendingDeletion // 5, ArtifactStatusPendingDeletion is the status of an artifact that is pending deletion + ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted ) func init() { @@ -147,8 +149,28 @@ func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) { Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts) } +// ListPendingDeleteArtifacts returns all artifacts in pending-delete status. +// limit is the max number of artifacts to return. +func ListPendingDeleteArtifacts(ctx context.Context, limit int) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, limit) + return arts, db.GetEngine(ctx). + Where("status = ?", ArtifactStatusPendingDeletion).Limit(limit).Find(&arts) +} + // SetArtifactExpired sets an artifact to expired func SetArtifactExpired(ctx context.Context, artifactID int64) error { _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) return err } + +// SetArtifactNeedDelete sets an artifact to need-delete, cron job will delete it +func SetArtifactNeedDelete(ctx context.Context, runID int64, name string) error { + _, err := db.GetEngine(ctx).Where("run_id=? AND artifact_name=? AND status = ?", runID, name, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)}) + return err +} + +// SetArtifactDeleted sets an artifact to deleted +func SetArtifactDeleted(ctx context.Context, artifactID int64) error { + _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)}) + return err +} diff --git a/models/actions/run.go b/models/actions/run.go index fcac58d515..fa9db0b554 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -170,15 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err return err } -// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. -func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { - // Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. +// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event. +// It's useful when a new run is triggered, and all previous runs needn't be continued anymore. +func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { + // Find all runs in the specified repository, reference, and workflow with non-final status runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ RepoID: repoID, Ref: ref, WorkflowID: workflowID, TriggerEvent: event, - Status: []Status{StatusRunning, StatusWaiting}, + Status: []Status{StatusRunning, StatusWaiting, StatusBlocked}, }) if err != nil { return err @@ -339,6 +340,23 @@ func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error) return run, nil } +func GetWorkflowLatestRun(ctx context.Context, repoID int64, workflowFile, branch, event string) (*ActionRun, error) { + var run ActionRun + q := db.GetEngine(ctx).Where("repo_id=?", repoID). + And("ref = ?", branch). + And("workflow_id = ?", workflowFile) + if event != "" { + q.And("event = ?", event) + } + has, err := q.Desc("id").Get(&run) + if err != nil { + return nil, err + } else if !has { + return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile) + } + return &run, nil +} + // UpdateRun updates a run. // It requires the inputted run has Version set. // It will return error if the version is not matched (it means the run has been changed after loaded). diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 6ea6cb9d3b..6c5d3b3252 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -16,14 +16,9 @@ import ( type ActionJobList []*ActionRunJob func (jobs ActionJobList) GetRunIDs() []int64 { - ids := make(container.Set[int64], len(jobs)) - for _, j := range jobs { - if j.RunID == 0 { - continue - } - ids.Add(j.RunID) - } - return ids.Values() + return container.FilterSlice(jobs, func(j *ActionRunJob) (int64, bool) { + return j.RunID, j.RunID != 0 + }) } func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 388bfc4f86..4046c7d369 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -19,19 +19,15 @@ type RunList []*ActionRun // GetUserIDs returns a slice of user's id func (runs RunList) GetUserIDs() []int64 { - ids := make(container.Set[int64], len(runs)) - for _, run := range runs { - ids.Add(run.TriggerUserID) - } - return ids.Values() + return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) { + return run.TriggerUserID, true + }) } func (runs RunList) GetRepoIDs() []int64 { - ids := make(container.Set[int64], len(runs)) - for _, run := range runs { - ids.Add(run.RepoID) - } - return ids.Values() + return container.FilterSlice(runs, func(run *ActionRun) (int64, bool) { + return run.RepoID, true + }) } func (runs RunList) LoadTriggerUser(ctx context.Context) error { diff --git a/models/actions/runner.go b/models/actions/runner.go index 4103ba4477..67f003387b 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -13,6 +13,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -97,7 +98,7 @@ func (r *ActionRunner) StatusName() string { } func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { - return lang.Tr("actions.runners.status." + r.StatusName()) + return lang.TrString("actions.runners.status." + r.StatusName()) } func (r *ActionRunner) IsOnline() bool { @@ -159,7 +160,7 @@ type FindRunnerOptions struct { OwnerID int64 Sort string Filter string - IsOnline util.OptionalBool + IsOnline optional.Option[bool] WithAvailable bool // not only runners belong to, but also runners can be used } @@ -186,10 +187,12 @@ func (opts FindRunnerOptions) ToConds() builder.Cond { cond = cond.And(builder.Like{"name", opts.Filter}) } - if opts.IsOnline.IsTrue() { - cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) - } else if opts.IsOnline.IsFalse() { - cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + if opts.IsOnline.Has() { + if opts.IsOnline.Value() { + cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + } else { + cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) + } } return cond } diff --git a/models/actions/runner_list.go b/models/actions/runner_list.go index 87f0886b47..3ef8ebb254 100644 --- a/models/actions/runner_list.go +++ b/models/actions/runner_list.go @@ -16,14 +16,9 @@ type RunnerList []*ActionRunner // GetUserIDs returns a slice of user's id func (runners RunnerList) GetUserIDs() []int64 { - ids := make(container.Set[int64], len(runners)) - for _, runner := range runners { - if runner.OwnerID == 0 { - continue - } - ids.Add(runner.OwnerID) - } - return ids.Values() + return container.FilterSlice(runners, func(runner *ActionRunner) (int64, bool) { + return runner.OwnerID, runner.OwnerID != 0 + }) } func (runners RunnerList) LoadOwners(ctx context.Context) error { @@ -41,16 +36,9 @@ func (runners RunnerList) LoadOwners(ctx context.Context) error { } func (runners RunnerList) getRepoIDs() []int64 { - repoIDs := make(container.Set[int64], len(runners)) - for _, runner := range runners { - if runner.RepoID == 0 { - continue - } - if _, ok := repoIDs[runner.RepoID]; !ok { - repoIDs[runner.RepoID] = struct{}{} - } - } - return repoIDs.Values() + return container.FilterSlice(runners, func(runner *ActionRunner) (int64, bool) { + return runner.RepoID, runner.RepoID > 0 + }) } func (runners RunnerList) LoadRepos(ctx context.Context) error { diff --git a/models/actions/schedule.go b/models/actions/schedule.go index d450e7aa07..3646a046a0 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -127,14 +127,14 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) er return fmt.Errorf("DeleteCronTaskByRepo: %v", err) } // cancel running cron jobs of this repository and delete old schedules - if err := CancelRunningJobs( + if err := CancelPreviousJobs( ctx, repo.ID, repo.DefaultBranch, "", webhook_module.HookEventSchedule, ); err != nil { - return fmt.Errorf("CancelRunningJobs: %v", err) + return fmt.Errorf("CancelPreviousJobs: %v", err) } return nil } diff --git a/models/actions/schedule_list.go b/models/actions/schedule_list.go index b806550b87..5361b94801 100644 --- a/models/actions/schedule_list.go +++ b/models/actions/schedule_list.go @@ -18,19 +18,15 @@ type ScheduleList []*ActionSchedule // GetUserIDs returns a slice of user's id func (schedules ScheduleList) GetUserIDs() []int64 { - ids := make(container.Set[int64], len(schedules)) - for _, schedule := range schedules { - ids.Add(schedule.TriggerUserID) - } - return ids.Values() + return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) { + return schedule.TriggerUserID, true + }) } func (schedules ScheduleList) GetRepoIDs() []int64 { - ids := make(container.Set[int64], len(schedules)) - for _, schedule := range schedules { - ids.Add(schedule.RepoID) - } - return ids.Values() + return container.FilterSlice(schedules, func(schedule *ActionSchedule) (int64, bool) { + return schedule.RepoID, true + }) } func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error { @@ -44,6 +40,9 @@ func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error { schedule.TriggerUser = user_model.NewActionsUser() } else { schedule.TriggerUser = users[schedule.TriggerUserID] + if schedule.TriggerUser == nil { + schedule.TriggerUser = user_model.NewGhostUser() + } } } return nil diff --git a/models/actions/schedule_spec_list.go b/models/actions/schedule_spec_list.go index e9ae268a6e..f7dac72f8b 100644 --- a/models/actions/schedule_spec_list.go +++ b/models/actions/schedule_spec_list.go @@ -16,11 +16,9 @@ import ( type SpecList []*ActionScheduleSpec func (specs SpecList) GetScheduleIDs() []int64 { - ids := make(container.Set[int64], len(specs)) - for _, spec := range specs { - ids.Add(spec.ScheduleID) - } - return ids.Values() + return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) { + return spec.ScheduleID, true + }) } func (specs SpecList) LoadSchedules(ctx context.Context) error { @@ -46,11 +44,9 @@ func (specs SpecList) LoadSchedules(ctx context.Context) error { } func (specs SpecList) GetRepoIDs() []int64 { - ids := make(container.Set[int64], len(specs)) - for _, spec := range specs { - ids.Add(spec.RepoID) - } - return ids.Values() + return container.FilterSlice(specs, func(spec *ActionScheduleSpec) (int64, bool) { + return spec.RepoID, true + }) } func (specs SpecList) LoadRepos(ctx context.Context) error { diff --git a/models/actions/status.go b/models/actions/status.go index c97578f2ac..eda2234137 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -41,7 +41,7 @@ func (s Status) String() string { // LocaleString returns the locale string name of the Status func (s Status) LocaleString(lang translation.Locale) string { - return lang.Tr("actions.status." + s.String()) + return lang.TrString("actions.status." + s.String()) } // IsDone returns whether the Status is final diff --git a/models/actions/task.go b/models/actions/task.go index 96a6d2e80c..9946cf5233 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -11,6 +11,7 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -227,7 +228,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask if runner.RepoID != 0 { jobCond = builder.Eq{"repo_id": runner.RepoID} } else if runner.OwnerID != 0 { - jobCond = builder.In("repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": runner.OwnerID})) + jobCond = builder.In("repo_id", builder.Select("`repository`.id").From("repository"). + Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id"). + Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions})) } if jobCond.IsValid() { jobCond = builder.In("run_id", builder.Select("id").From("action_run").Where(jobCond)) diff --git a/models/actions/task_list.go b/models/actions/task_list.go index b07d00b8db..5e17f91441 100644 --- a/models/actions/task_list.go +++ b/models/actions/task_list.go @@ -16,14 +16,9 @@ import ( type TaskList []*ActionTask func (tasks TaskList) GetJobIDs() []int64 { - ids := make(container.Set[int64], len(tasks)) - for _, t := range tasks { - if t.JobID == 0 { - continue - } - ids.Add(t.JobID) - } - return ids.Values() + return container.FilterSlice(tasks, func(t *ActionTask) (int64, bool) { + return t.JobID, t.JobID != 0 + }) } func (tasks TaskList) LoadJobs(ctx context.Context) error { diff --git a/models/actions/variable.go b/models/actions/variable.go index 12717e0ae4..b0a455e675 100644 --- a/models/actions/variable.go +++ b/models/actions/variable.go @@ -6,12 +6,11 @@ package actions import ( "context" "errors" - "fmt" "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -54,24 +53,24 @@ type FindVariablesOpts struct { db.ListOptions OwnerID int64 RepoID int64 + Name string } func (opts FindVariablesOpts) ToConds() builder.Cond { cond := builder.NewCond() + // Since we now support instance-level variables, + // there is no need to check for null values for `owner_id` and `repo_id` cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + + if opts.Name != "" { + cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) + } return cond } -func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) { - var variable ActionVariable - has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable) - if err != nil { - return nil, err - } else if !has { - return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist) - } - return &variable, nil +func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) { + return db.Find[ActionVariable](ctx, opts) } func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { @@ -82,3 +81,42 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) }) return count != 0, err } + +func DeleteVariable(ctx context.Context, id int64) error { + if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil { + return err + } + return nil +} + +func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) { + variables := map[string]string{} + + // Global + globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{}) + if err != nil { + log.Error("find global variables: %v", err) + return nil, err + } + + // Org / User level + ownerVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{OwnerID: run.Repo.OwnerID}) + if err != nil { + log.Error("find variables of org: %d, error: %v", run.Repo.OwnerID, err) + return nil, err + } + + // Repo level + repoVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{RepoID: run.RepoID}) + if err != nil { + log.Error("find variables of repo: %d, error: %v", run.RepoID, err) + return nil, err + } + + // Level precedence: Repo > Org / User > Global + for _, v := range append(globalVariables, append(ownerVariables, repoVariables...)...) { + variables[v.Name] = v.Data + } + + return variables, nil +} diff --git a/models/activities/action.go b/models/activities/action.go index 15bd9a52ac..7e2ef4c9ae 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -148,6 +148,7 @@ type Action struct { Repo *repo_model.Repository `xorm:"-"` CommentID int64 `xorm:"INDEX"` Comment *issues_model.Comment `xorm:"-"` + Issue *issues_model.Issue `xorm:"-"` // get the issue id from content IsDeleted bool `xorm:"NOT NULL DEFAULT false"` RefName string IsPrivate bool `xorm:"NOT NULL DEFAULT false"` @@ -225,8 +226,8 @@ func (a *Action) ShortActUserName(ctx context.Context) string { return base.EllipsisString(a.GetActUserName(ctx), 20) } -// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. -func (a *Action) GetDisplayName(ctx context.Context) string { +// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank. +func (a *Action) GetActDisplayName(ctx context.Context) string { if setting.UI.DefaultShowFullName { trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx)) if len(trimmedFullName) > 0 { @@ -236,8 +237,8 @@ func (a *Action) GetDisplayName(ctx context.Context) string { return a.ShortActUserName(ctx) } -// GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME -func (a *Action) GetDisplayNameTitle(ctx context.Context) string { +// GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME +func (a *Action) GetActDisplayNameTitle(ctx context.Context) string { if setting.UI.DefaultShowFullName { return a.ShortActUserName(ctx) } @@ -290,11 +291,6 @@ func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string { return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx)) } -// GetCommentHTMLURL returns link to action comment. -func (a *Action) GetCommentHTMLURL(ctx context.Context) string { - return a.getCommentHTMLURL(ctx) -} - func (a *Action) loadComment(ctx context.Context) (err error) { if a.CommentID == 0 || a.Comment != nil { return nil @@ -303,7 +299,8 @@ func (a *Action) loadComment(ctx context.Context) (err error) { return err } -func (a *Action) getCommentHTMLURL(ctx context.Context) string { +// GetCommentHTMLURL returns link to action comment. +func (a *Action) GetCommentHTMLURL(ctx context.Context) string { if a == nil { return "#" } @@ -311,34 +308,19 @@ func (a *Action) getCommentHTMLURL(ctx context.Context) string { if a.Comment != nil { return a.Comment.HTMLURL(ctx) } - if len(a.GetIssueInfos()) == 0 { + + if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { return "#" } - // Return link to issue - issueIDString := a.GetIssueInfos()[0] - issueID, err := strconv.ParseInt(issueIDString, 10, 64) - if err != nil { + if err := a.Issue.LoadRepo(ctx); err != nil { return "#" } - issue, err := issues_model.GetIssueByID(ctx, issueID) - if err != nil { - return "#" - } - - if err = issue.LoadRepo(ctx); err != nil { - return "#" - } - - return issue.HTMLURL() + return a.Issue.HTMLURL() } // GetCommentLink returns link to action comment. func (a *Action) GetCommentLink(ctx context.Context) string { - return a.getCommentLink(ctx) -} - -func (a *Action) getCommentLink(ctx context.Context) string { if a == nil { return "#" } @@ -346,26 +328,15 @@ func (a *Action) getCommentLink(ctx context.Context) string { if a.Comment != nil { return a.Comment.Link(ctx) } - if len(a.GetIssueInfos()) == 0 { + + if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { return "#" } - // Return link to issue - issueIDString := a.GetIssueInfos()[0] - issueID, err := strconv.ParseInt(issueIDString, 10, 64) - if err != nil { + if err := a.Issue.LoadRepo(ctx); err != nil { return "#" } - issue, err := issues_model.GetIssueByID(ctx, issueID) - if err != nil { - return "#" - } - - if err = issue.LoadRepo(ctx); err != nil { - return "#" - } - - return issue.Link() + return a.Issue.Link() } // GetBranch returns the action's repository branch. @@ -393,33 +364,66 @@ func (a *Action) GetCreate() time.Time { return a.CreatedUnix.AsTime() } -// GetIssueInfos returns a list of issues associated with -// the action. +func (a *Action) IsIssueEvent() bool { + return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request") +} + +// GetIssueInfos returns a list of associated information with the action. func (a *Action) GetIssueInfos() []string { - return strings.SplitN(a.Content, "|", 3) + // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length + ret := strings.SplitN(a.Content, "|", 3) + for len(ret) < 3 { + ret = append(ret, "") + } + return ret +} + +func (a *Action) getIssueIndex() int64 { + infos := a.GetIssueInfos() + if len(infos) == 0 { + return 0 + } + index, _ := strconv.ParseInt(infos[0], 10, 64) + return index +} + +func (a *Action) LoadIssue(ctx context.Context) error { + if a.Issue != nil { + return nil + } + if index := a.getIssueIndex(); index > 0 { + issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) + if err != nil { + return err + } + a.Issue = issue + a.Issue.Repo = a.Repo + } + return nil } // GetIssueTitle returns the title of first issue associated with the action. func (a *Action) GetIssueTitle(ctx context.Context) string { - index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) - issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) - if err != nil { - log.Error("GetIssueByIndex: %v", err) - return "500 when get issue" + if err := a.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return "<500 when get issue>" } - return issue.Title + if a.Issue == nil { + return "<Issue not found>" + } + return a.Issue.Title } -// GetIssueContent returns the content of first issue associated with -// this action. +// GetIssueContent returns the content of first issue associated with this action. func (a *Action) GetIssueContent(ctx context.Context) string { - index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) - issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) - if err != nil { - log.Error("GetIssueByIndex: %v", err) - return "500 when get issue" + if err := a.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return "<500 when get issue>" } - return issue.Content + if a.Issue == nil { + return "<Content not found>" + } + return a.Issue.Content } // GetFeedsOptions options for retrieving feeds @@ -459,7 +463,7 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return nil, 0, fmt.Errorf("FindAndCount: %w", err) } - if err := ActionList(actions).loadAttributes(ctx); err != nil { + if err := ActionList(actions).LoadAttributes(ctx); err != nil { return nil, 0, fmt.Errorf("LoadAttributes: %w", err) } diff --git a/models/activities/action_list.go b/models/activities/action_list.go index 3d74397c69..6e23b173b5 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -6,25 +6,28 @@ package activities import ( "context" "fmt" + "strconv" "code.gitea.io/gitea/models/db" + 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/container" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" ) // ActionList defines a list of actions type ActionList []*Action func (actions ActionList) getUserIDs() []int64 { - userIDs := make(container.Set[int64], len(actions)) - for _, action := range actions { - userIDs.Add(action.ActUserID) - } - return userIDs.Values() + return container.FilterSlice(actions, func(action *Action) (int64, bool) { + return action.ActUserID, true + }) } -func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model.User, error) { +func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) { if len(actions) == 0 { return nil, nil } @@ -45,14 +48,12 @@ func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model. } func (actions ActionList) getRepoIDs() []int64 { - repoIDs := make(container.Set[int64], len(actions)) - for _, action := range actions { - repoIDs.Add(action.RepoID) - } - return repoIDs.Values() + return container.FilterSlice(actions, func(action *Action) (int64, bool) { + return action.RepoID, true + }) } -func (actions ActionList) loadRepositories(ctx context.Context) error { +func (actions ActionList) LoadRepositories(ctx context.Context) error { if len(actions) == 0 { return nil } @@ -63,11 +64,11 @@ func (actions ActionList) loadRepositories(ctx context.Context) error { if err != nil { return fmt.Errorf("find repository: %w", err) } - for _, action := range actions { action.Repo = repoMaps[action.RepoID] } - return nil + repos := repo_model.RepositoryList(util.ValuesOfMap(repoMaps)) + return repos.LoadUnits(ctx) } func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) { @@ -75,37 +76,122 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]* userMap = make(map[int64]*user_model.User) } - for _, action := range actions { + missingUserIDs := container.FilterSlice(actions, func(action *Action) (int64, bool) { if action.Repo == nil { - continue + return 0, false } - repoOwner, ok := userMap[action.Repo.OwnerID] - if !ok { - repoOwner, err = user_model.GetUserByID(ctx, action.Repo.OwnerID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - continue - } - return err - } - userMap[repoOwner.ID] = repoOwner + _, alreadyLoaded := userMap[action.Repo.OwnerID] + return action.Repo.OwnerID, !alreadyLoaded + }) + + if err := db.GetEngine(ctx). + In("id", missingUserIDs). + Find(&userMap); err != nil { + return fmt.Errorf("find user: %w", err) + } + + for _, action := range actions { + if action.Repo != nil { + action.Repo.Owner = userMap[action.Repo.OwnerID] } - action.Repo.Owner = repoOwner } return nil } -// loadAttributes loads all attributes -func (actions ActionList) loadAttributes(ctx context.Context) error { - userMap, err := actions.loadUsers(ctx) +// LoadAttributes loads all attributes +func (actions ActionList) LoadAttributes(ctx context.Context) error { + // the load sequence cannot be changed because of the dependencies + userMap, err := actions.LoadActUsers(ctx) if err != nil { return err } - - if err := actions.loadRepositories(ctx); err != nil { + if err := actions.LoadRepositories(ctx); err != nil { return err } - - return actions.loadRepoOwner(ctx, userMap) + if err := actions.loadRepoOwner(ctx, userMap); err != nil { + return err + } + if err := actions.LoadIssues(ctx); err != nil { + return err + } + return actions.LoadComments(ctx) +} + +func (actions ActionList) LoadComments(ctx context.Context) error { + if len(actions) == 0 { + return nil + } + + commentIDs := make([]int64, 0, len(actions)) + for _, action := range actions { + if action.CommentID > 0 { + commentIDs = append(commentIDs, action.CommentID) + } + } + + commentsMap := make(map[int64]*issues_model.Comment, len(commentIDs)) + if err := db.GetEngine(ctx).In("id", commentIDs).Find(&commentsMap); err != nil { + return fmt.Errorf("find comment: %w", err) + } + + for _, action := range actions { + if action.CommentID > 0 { + action.Comment = commentsMap[action.CommentID] + if action.Comment != nil { + action.Comment.Issue = action.Issue + } + } + } + return nil +} + +func (actions ActionList) LoadIssues(ctx context.Context) error { + if len(actions) == 0 { + return nil + } + + conditions := builder.NewCond() + issueNum := 0 + for _, action := range actions { + if action.IsIssueEvent() { + infos := action.GetIssueInfos() + if len(infos) == 0 { + continue + } + index, _ := strconv.ParseInt(infos[0], 10, 64) + if index > 0 { + conditions = conditions.Or(builder.Eq{ + "repo_id": action.RepoID, + "`index`": index, + }) + issueNum++ + } + } + } + if !conditions.IsValid() { + return nil + } + + issuesMap := make(map[string]*issues_model.Issue, issueNum) + issues := make([]*issues_model.Issue, 0, issueNum) + if err := db.GetEngine(ctx).Where(conditions).Find(&issues); err != nil { + return fmt.Errorf("find issue: %w", err) + } + for _, issue := range issues { + issuesMap[fmt.Sprintf("%d-%d", issue.RepoID, issue.Index)] = issue + } + + for _, action := range actions { + if !action.IsIssueEvent() { + continue + } + if index := action.getIssueIndex(); index > 0 { + if issue, ok := issuesMap[fmt.Sprintf("%d-%d", action.RepoID, index)]; ok { + action.Issue = issue + action.Issue.Repo = action.Repo + } + } + } + return nil } diff --git a/models/activities/notification.go b/models/activities/notification.go index 230bcdd6e8..dc1b8c6fae 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -12,12 +12,8 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" - 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/container" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -79,53 +75,6 @@ func init() { db.RegisterModel(new(Notification)) } -// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored. -type FindNotificationOptions struct { - db.ListOptions - UserID int64 - RepoID int64 - IssueID int64 - Status []NotificationStatus - Source []NotificationSource - UpdatedAfterUnix int64 - UpdatedBeforeUnix int64 -} - -// ToCond will convert each condition into a xorm-Cond -func (opts FindNotificationOptions) ToConds() builder.Cond { - cond := builder.NewCond() - if opts.UserID != 0 { - cond = cond.And(builder.Eq{"notification.user_id": opts.UserID}) - } - if opts.RepoID != 0 { - cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID}) - } - if opts.IssueID != 0 { - cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) - } - if len(opts.Status) > 0 { - if len(opts.Status) == 1 { - cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) - } else { - cond = cond.And(builder.In("notification.status", opts.Status)) - } - } - if len(opts.Source) > 0 { - cond = cond.And(builder.In("notification.source", opts.Source)) - } - if opts.UpdatedAfterUnix != 0 { - cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) - } - if opts.UpdatedBeforeUnix != 0 { - cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix}) - } - return cond -} - -func (opts FindNotificationOptions) ToOrders() string { - return "notification.updated_unix DESC" -} - // CreateRepoTransferNotification creates notification for the user a repository was transferred to func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error { return db.WithTx(ctx, func(ctx context.Context) error { @@ -159,109 +108,6 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo }) } -// CreateOrUpdateIssueNotifications creates an issue notification -// for each watcher, or updates it if already exists -// receiverID > 0 just send to receiver, else send to all watcher -func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { - return err - } - - return committer.Commit() -} - -func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { - // init - var toNotify container.Set[int64] - notifications, err := db.Find[Notification](ctx, FindNotificationOptions{ - IssueID: issueID, - }) - if err != nil { - return err - } - - issue, err := issues_model.GetIssueByID(ctx, issueID) - if err != nil { - return err - } - - if receiverID > 0 { - toNotify = make(container.Set[int64], 1) - toNotify.Add(receiverID) - } else { - toNotify = make(container.Set[int64], 32) - issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) - if err != nil { - return err - } - toNotify.AddMultiple(issueWatches...) - if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { - repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) - if err != nil { - return err - } - toNotify.AddMultiple(repoWatches...) - } - issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) - if err != nil { - return err - } - toNotify.AddMultiple(issueParticipants...) - - // dont notify user who cause notification - delete(toNotify, notificationAuthorID) - // explicit unwatch on issue - issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) - if err != nil { - return err - } - for _, id := range issueUnWatches { - toNotify.Remove(id) - } - } - - err = issue.LoadRepo(ctx) - if err != nil { - return err - } - - // notify - for userID := range toNotify { - issue.Repo.Units = nil - user, err := user_model.GetUserByID(ctx, userID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - continue - } - - return err - } - if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { - continue - } - if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { - continue - } - - if notificationExists(notifications, issue.ID, userID) { - if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { - return err - } - continue - } - if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil { - return err - } - } - return nil -} - func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error { notification := &Notification{ UserID: userID, @@ -449,309 +295,6 @@ func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.Tim return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res) } -// NotificationList contains a list of notifications -type NotificationList []*Notification - -// LoadAttributes load Repo Issue User and Comment if not loaded -func (nl NotificationList) LoadAttributes(ctx context.Context) error { - if _, _, err := nl.LoadRepos(ctx); err != nil { - return err - } - if _, err := nl.LoadIssues(ctx); err != nil { - return err - } - if _, err := nl.LoadUsers(ctx); err != nil { - return err - } - if _, err := nl.LoadComments(ctx); err != nil { - return err - } - return nil -} - -func (nl NotificationList) getPendingRepoIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.Repository != nil { - continue - } - ids.Add(notification.RepoID) - } - return ids.Values() -} - -// LoadRepos loads repositories from database -func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) { - if len(nl) == 0 { - return repo_model.RepositoryList{}, []int{}, nil - } - - repoIDs := nl.getPendingRepoIDs() - repos := make(map[int64]*repo_model.Repository, len(repoIDs)) - left := len(repoIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", repoIDs[:limit]). - Rows(new(repo_model.Repository)) - if err != nil { - return nil, nil, err - } - - for rows.Next() { - var repo repo_model.Repository - err = rows.Scan(&repo) - if err != nil { - rows.Close() - return nil, nil, err - } - - repos[repo.ID] = &repo - } - _ = rows.Close() - - left -= limit - repoIDs = repoIDs[limit:] - } - - failed := []int{} - - reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) - for i, notification := range nl { - if notification.Repository == nil { - notification.Repository = repos[notification.RepoID] - } - if notification.Repository == nil { - log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID) - failed = append(failed, i) - continue - } - var found bool - for _, r := range reposList { - if r.ID == notification.RepoID { - found = true - break - } - } - if !found { - reposList = append(reposList, notification.Repository) - } - } - return reposList, failed, nil -} - -func (nl NotificationList) getPendingIssueIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.Issue != nil { - continue - } - ids.Add(notification.IssueID) - } - return ids.Values() -} - -// LoadIssues loads issues from database -func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { - if len(nl) == 0 { - return []int{}, nil - } - - issueIDs := nl.getPendingIssueIDs() - issues := make(map[int64]*issues_model.Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", issueIDs[:limit]). - Rows(new(issues_model.Issue)) - if err != nil { - return nil, err - } - - for rows.Next() { - var issue issues_model.Issue - err = rows.Scan(&issue) - if err != nil { - rows.Close() - return nil, err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] - } - - failures := []int{} - - for i, notification := range nl { - if notification.Issue == nil { - notification.Issue = issues[notification.IssueID] - if notification.Issue == nil { - if notification.IssueID != 0 { - log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID) - failures = append(failures, i) - } - continue - } - notification.Issue.Repo = notification.Repository - } - } - return failures, nil -} - -// Without returns the notification list without the failures -func (nl NotificationList) Without(failures []int) NotificationList { - if len(failures) == 0 { - return nl - } - remaining := make([]*Notification, 0, len(nl)) - last := -1 - var i int - for _, i = range failures { - remaining = append(remaining, nl[last+1:i]...) - last = i - } - if len(nl) > i { - remaining = append(remaining, nl[i+1:]...) - } - return remaining -} - -func (nl NotificationList) getPendingCommentIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.CommentID == 0 || notification.Comment != nil { - continue - } - ids.Add(notification.CommentID) - } - return ids.Values() -} - -func (nl NotificationList) getUserIDs() []int64 { - ids := make(container.Set[int64], len(nl)) - for _, notification := range nl { - if notification.UserID == 0 || notification.User != nil { - continue - } - ids.Add(notification.UserID) - } - return ids.Values() -} - -// LoadUsers loads users from database -func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { - if len(nl) == 0 { - return []int{}, nil - } - - userIDs := nl.getUserIDs() - users := make(map[int64]*user_model.User, len(userIDs)) - left := len(userIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", userIDs[:limit]). - Rows(new(user_model.User)) - if err != nil { - return nil, err - } - - for rows.Next() { - var user user_model.User - err = rows.Scan(&user) - if err != nil { - rows.Close() - return nil, err - } - - users[user.ID] = &user - } - _ = rows.Close() - - left -= limit - userIDs = userIDs[limit:] - } - - failures := []int{} - for i, notification := range nl { - if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil { - notification.User = users[notification.UserID] - if notification.User == nil { - log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID) - failures = append(failures, i) - continue - } - } - } - return failures, nil -} - -// LoadComments loads comments from database -func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { - if len(nl) == 0 { - return []int{}, nil - } - - commentIDs := nl.getPendingCommentIDs() - comments := make(map[int64]*issues_model.Comment, len(commentIDs)) - left := len(commentIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", commentIDs[:limit]). - Rows(new(issues_model.Comment)) - if err != nil { - return nil, err - } - - for rows.Next() { - var comment issues_model.Comment - err = rows.Scan(&comment) - if err != nil { - rows.Close() - return nil, err - } - - comments[comment.ID] = &comment - } - _ = rows.Close() - - left -= limit - commentIDs = commentIDs[limit:] - } - - failures := []int{} - for i, notification := range nl { - if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil { - notification.Comment = comments[notification.CommentID] - if notification.Comment == nil { - log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID) - failures = append(failures, i) - continue - } - notification.Comment.Issue = notification.Issue - } - } - return failures, nil -} - // SetIssueReadBy sets issue to be read by given user. func SetIssueReadBy(ctx context.Context, issueID, userID int64) error { if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil { diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go new file mode 100644 index 0000000000..0cbb91df3c --- /dev/null +++ b/models/activities/notification_list.go @@ -0,0 +1,499 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package activities + +import ( + "context" + + "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" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored. +type FindNotificationOptions struct { + db.ListOptions + UserID int64 + RepoID int64 + IssueID int64 + Status []NotificationStatus + Source []NotificationSource + UpdatedAfterUnix int64 + UpdatedBeforeUnix int64 +} + +// ToCond will convert each condition into a xorm-Cond +func (opts FindNotificationOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.UserID != 0 { + cond = cond.And(builder.Eq{"notification.user_id": opts.UserID}) + } + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID}) + } + if opts.IssueID != 0 { + cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) + } + if len(opts.Status) > 0 { + if len(opts.Status) == 1 { + cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) + } else { + cond = cond.And(builder.In("notification.status", opts.Status)) + } + } + if len(opts.Source) > 0 { + cond = cond.And(builder.In("notification.source", opts.Source)) + } + if opts.UpdatedAfterUnix != 0 { + cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) + } + if opts.UpdatedBeforeUnix != 0 { + cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix}) + } + return cond +} + +func (opts FindNotificationOptions) ToOrders() string { + return "notification.updated_unix DESC" +} + +// CreateOrUpdateIssueNotifications creates an issue notification +// for each watcher, or updates it if already exists +// receiverID > 0 just send to receiver, else send to all watcher +func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { + return err + } + + return committer.Commit() +} + +func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { + // init + var toNotify container.Set[int64] + notifications, err := db.Find[Notification](ctx, FindNotificationOptions{ + IssueID: issueID, + }) + if err != nil { + return err + } + + issue, err := issues_model.GetIssueByID(ctx, issueID) + if err != nil { + return err + } + + if receiverID > 0 { + toNotify = make(container.Set[int64], 1) + toNotify.Add(receiverID) + } else { + toNotify = make(container.Set[int64], 32) + issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) + if err != nil { + return err + } + toNotify.AddMultiple(issueWatches...) + if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { + repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) + if err != nil { + return err + } + toNotify.AddMultiple(repoWatches...) + } + issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) + if err != nil { + return err + } + toNotify.AddMultiple(issueParticipants...) + + // dont notify user who cause notification + delete(toNotify, notificationAuthorID) + // explicit unwatch on issue + issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) + if err != nil { + return err + } + for _, id := range issueUnWatches { + toNotify.Remove(id) + } + } + + err = issue.LoadRepo(ctx) + if err != nil { + return err + } + + // notify + for userID := range toNotify { + issue.Repo.Units = nil + user, err := user_model.GetUserByID(ctx, userID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + continue + } + + return err + } + if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { + continue + } + if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { + continue + } + + if notificationExists(notifications, issue.ID, userID) { + if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { + return err + } + continue + } + if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil { + return err + } + } + return nil +} + +// NotificationList contains a list of notifications +type NotificationList []*Notification + +// LoadAttributes load Repo Issue User and Comment if not loaded +func (nl NotificationList) LoadAttributes(ctx context.Context) error { + if _, _, err := nl.LoadRepos(ctx); err != nil { + return err + } + if _, err := nl.LoadIssues(ctx); err != nil { + return err + } + if _, err := nl.LoadUsers(ctx); err != nil { + return err + } + if _, err := nl.LoadComments(ctx); err != nil { + return err + } + return nil +} + +func (nl NotificationList) getPendingRepoIDs() []int64 { + return container.FilterSlice(nl, func(n *Notification) (int64, bool) { + if n.Repository != nil { + return 0, false + } + return n.RepoID, true + }) +} + +// LoadRepos loads repositories from database +func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) { + if len(nl) == 0 { + return repo_model.RepositoryList{}, []int{}, nil + } + + repoIDs := nl.getPendingRepoIDs() + repos := make(map[int64]*repo_model.Repository, len(repoIDs)) + left := len(repoIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", repoIDs[:limit]). + Rows(new(repo_model.Repository)) + if err != nil { + return nil, nil, err + } + + for rows.Next() { + var repo repo_model.Repository + err = rows.Scan(&repo) + if err != nil { + rows.Close() + return nil, nil, err + } + + repos[repo.ID] = &repo + } + _ = rows.Close() + + left -= limit + repoIDs = repoIDs[limit:] + } + + failed := []int{} + + reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) + for i, notification := range nl { + if notification.Repository == nil { + notification.Repository = repos[notification.RepoID] + } + if notification.Repository == nil { + log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID) + failed = append(failed, i) + continue + } + var found bool + for _, r := range reposList { + if r.ID == notification.RepoID { + found = true + break + } + } + if !found { + reposList = append(reposList, notification.Repository) + } + } + return reposList, failed, nil +} + +func (nl NotificationList) getPendingIssueIDs() []int64 { + ids := make(container.Set[int64], len(nl)) + for _, notification := range nl { + if notification.Issue != nil { + continue + } + ids.Add(notification.IssueID) + } + return ids.Values() +} + +// LoadIssues loads issues from database +func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { + if len(nl) == 0 { + return []int{}, nil + } + + issueIDs := nl.getPendingIssueIDs() + issues := make(map[int64]*issues_model.Issue, len(issueIDs)) + left := len(issueIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", issueIDs[:limit]). + Rows(new(issues_model.Issue)) + if err != nil { + return nil, err + } + + for rows.Next() { + var issue issues_model.Issue + err = rows.Scan(&issue) + if err != nil { + rows.Close() + return nil, err + } + + issues[issue.ID] = &issue + } + _ = rows.Close() + + left -= limit + issueIDs = issueIDs[limit:] + } + + failures := []int{} + + for i, notification := range nl { + if notification.Issue == nil { + notification.Issue = issues[notification.IssueID] + if notification.Issue == nil { + if notification.IssueID != 0 { + log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID) + failures = append(failures, i) + } + continue + } + notification.Issue.Repo = notification.Repository + } + } + return failures, nil +} + +// Without returns the notification list without the failures +func (nl NotificationList) Without(failures []int) NotificationList { + if len(failures) == 0 { + return nl + } + remaining := make([]*Notification, 0, len(nl)) + last := -1 + var i int + for _, i = range failures { + remaining = append(remaining, nl[last+1:i]...) + last = i + } + if len(nl) > i { + remaining = append(remaining, nl[i+1:]...) + } + return remaining +} + +func (nl NotificationList) getPendingCommentIDs() []int64 { + ids := make(container.Set[int64], len(nl)) + for _, notification := range nl { + if notification.CommentID == 0 || notification.Comment != nil { + continue + } + ids.Add(notification.CommentID) + } + return ids.Values() +} + +func (nl NotificationList) getUserIDs() []int64 { + ids := make(container.Set[int64], len(nl)) + for _, notification := range nl { + if notification.UserID == 0 || notification.User != nil { + continue + } + ids.Add(notification.UserID) + } + return ids.Values() +} + +// LoadUsers loads users from database +func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { + if len(nl) == 0 { + return []int{}, nil + } + + userIDs := nl.getUserIDs() + users := make(map[int64]*user_model.User, len(userIDs)) + left := len(userIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", userIDs[:limit]). + Rows(new(user_model.User)) + if err != nil { + return nil, err + } + + for rows.Next() { + var user user_model.User + err = rows.Scan(&user) + if err != nil { + rows.Close() + return nil, err + } + + users[user.ID] = &user + } + _ = rows.Close() + + left -= limit + userIDs = userIDs[limit:] + } + + failures := []int{} + for i, notification := range nl { + if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil { + notification.User = users[notification.UserID] + if notification.User == nil { + log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID) + failures = append(failures, i) + continue + } + } + } + return failures, nil +} + +// LoadComments loads comments from database +func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { + if len(nl) == 0 { + return []int{}, nil + } + + commentIDs := nl.getPendingCommentIDs() + comments := make(map[int64]*issues_model.Comment, len(commentIDs)) + left := len(commentIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + rows, err := db.GetEngine(ctx). + In("id", commentIDs[:limit]). + Rows(new(issues_model.Comment)) + if err != nil { + return nil, err + } + + for rows.Next() { + var comment issues_model.Comment + err = rows.Scan(&comment) + if err != nil { + rows.Close() + return nil, err + } + + comments[comment.ID] = &comment + } + _ = rows.Close() + + left -= limit + commentIDs = commentIDs[limit:] + } + + failures := []int{} + for i, notification := range nl { + if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil { + notification.Comment = comments[notification.CommentID] + if notification.Comment == nil { + log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID) + failures = append(failures, i) + continue + } + notification.Comment.Issue = notification.Issue + } + } + return failures, nil +} + +// LoadIssuePullRequests loads all issues' pull requests if possible +func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error { + issues := make(map[int64]*issues_model.Issue, len(nl)) + for _, notification := range nl { + if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil { + issues[notification.Issue.ID] = notification.Issue + } + } + + if len(issues) == 0 { + return nil + } + + pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues)) + if err != nil { + return err + } + + for _, pull := range pulls { + if issue := issues[pull.IssueID]; issue != nil { + issue.PullRequest = pull + issue.PullRequest.Issue = issue + } + } + + return nil +} diff --git a/models/activities/statistic.go b/models/activities/statistic.go index fe5f7d0872..d1a459d1b2 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -9,6 +9,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "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" @@ -29,7 +30,8 @@ type Statistic struct { Mirror, Release, AuthSource, Webhook, Milestone, Label, HookTask, Team, UpdateTask, Project, - ProjectBoard, Attachment int64 + ProjectBoard, Attachment, + Branches, Tags, CommitStatus int64 IssueByLabel []IssueByLabelCount IssueByRepository []IssueByRepositoryCount } @@ -58,6 +60,9 @@ func GetStatistic(ctx context.Context) (stats Statistic) { stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) stats.Counter.Star, _ = e.Count(new(repo_model.Star)) stats.Counter.Access, _ = e.Count(new(access_model.Access)) + stats.Counter.Branches, _ = e.Count(new(git_model.Branch)) + stats.Counter.Tags, _ = e.Where("is_draft=?", false).Count(new(repo_model.Release)) + stats.Counter.CommitStatus, _ = e.Count(new(git_model.CommitStatus)) type IssueCount struct { Count int64 diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 83fbab5d36..06ac31bc6f 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -139,13 +139,7 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific } } - keyID := "" - if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { - keyID = fmt.Sprintf("%X", *sig.IssuerKeyId) - } - if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 { - keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20]) - } + keyID := tryGetKeyIDFromSignature(sig) defaultReason := NoKeyFound // First check if the sig has a keyID and if so just look at that diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go index b02be2851a..9c015582f1 100644 --- a/models/asymkey/gpg_key_common.go +++ b/models/asymkey/gpg_key_common.go @@ -134,3 +134,13 @@ func extractSignature(s string) (*packet.Signature, error) { } return sig, nil } + +func tryGetKeyIDFromSignature(sig *packet.Signature) string { + if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { + return fmt.Sprintf("%016X", *sig.IssuerKeyId) + } + if sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 { + return fmt.Sprintf("%016X", sig.IssuerFingerprint[12:20]) + } + return "" +} diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go index dee74bc281..d3fbb01d82 100644 --- a/models/asymkey/gpg_key_test.go +++ b/models/asymkey/gpg_key_test.go @@ -11,7 +11,9 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + "github.com/keybase/go-crypto/openpgp/packet" "github.com/stretchr/testify/assert" ) @@ -391,3 +393,13 @@ epiDVQ== assert.Equal(t, time.Unix(1586105389, 0), expire) } } + +func TestTryGetKeyIDFromSignature(t *testing.T) { + assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{})) + assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ + IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)), + })) + assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ + IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c}, + })) +} diff --git a/models/asymkey/gpg_key_verify.go b/models/asymkey/gpg_key_verify.go index 4cf46ab556..01812a2d54 100644 --- a/models/asymkey/gpg_key_verify.go +++ b/models/asymkey/gpg_key_verify.go @@ -46,6 +46,10 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st return "", ErrGPGKeyNotExist{} } + if err := key.LoadSubKeys(ctx); err != nil { + return "", err + } + sig, err := extractSignature(signature) if err != nil { return "", ErrGPGInvalidTokenSignature{ diff --git a/models/asymkey/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go index 267ab252c8..2e4cd62e5c 100644 --- a/models/asymkey/ssh_key_authorized_keys.go +++ b/models/asymkey/ssh_key_authorized_keys.go @@ -12,7 +12,6 @@ import ( "path/filepath" "strings" "sync" - "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" @@ -44,6 +43,12 @@ const ( var sshOpLocker sync.Mutex +func WithSSHOpLocker(f func() error) error { + sshOpLocker.Lock() + defer sshOpLocker.Unlock() + return f() +} + // AuthorizedStringForKey creates the authorized keys string appropriate for the provided key func AuthorizedStringForKey(key *PublicKey) string { sb := &strings.Builder{} @@ -114,65 +119,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error { return nil } -// 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 - } - - sshOpLocker.Lock() - defer sshOpLocker.Unlock() - - 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 := RegeneratePublicKeys(ctx, t); err != nil { - return err - } - - t.Close() - return util.Rename(tmpPath, fPath) -} - // RegeneratePublicKeys regenerates the authorized_keys file func RegeneratePublicKeys(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) { @@ -193,6 +139,8 @@ func RegeneratePublicKeys(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() @@ -202,11 +150,12 @@ func RegeneratePublicKeys(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("RegeneratePublicKeys scan: %w", err) + } } return nil } diff --git a/models/asymkey/ssh_key_fingerprint.go b/models/asymkey/ssh_key_fingerprint.go index b9cfb1b251..1ed3b5df2a 100644 --- a/models/asymkey/ssh_key_fingerprint.go +++ b/models/asymkey/ssh_key_fingerprint.go @@ -76,23 +76,14 @@ func calcFingerprintNative(publicKeyContent string) (string, error) { // CalcFingerprint calculate public key's fingerprint func CalcFingerprint(publicKeyContent string) (string, error) { // Call the method based on configuration - var ( - fnName, fp string - err error - ) - if len(setting.SSH.KeygenPath) == 0 { - fnName = "calcFingerprintNative" - fp, err = calcFingerprintNative(publicKeyContent) - } else { - fnName = "calcFingerprintSSHKeygen" - fp, err = calcFingerprintSSHKeygen(publicKeyContent) - } + useNative := setting.SSH.KeygenPath == "" + calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen) + fp, err := calcFn(publicKeyContent) if err != nil { if IsErrKeyUnableVerify(err) { - log.Info("%s", publicKeyContent) return "", err } - return "", fmt.Errorf("%s: %w", fnName, err) + return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err) } return fp, nil } diff --git a/models/asymkey/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go index 4e7dee2c91..e8b97d306e 100644 --- a/models/asymkey/ssh_key_principals.go +++ b/models/asymkey/ssh_key_principals.go @@ -9,51 +9,11 @@ import ( "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) -// AddPrincipalKey adds new principal to database and authorized_principals file. -func AddPrincipalKey(ctx context.Context, ownerID int64, content string, authSourceID int64) (*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, KeyTypePrincipal). - Get(new(PublicKey)) - if err != nil { - return nil, err - } else if has { - return nil, ErrKeyAlreadyExist{0, "", content} - } - - key := &PublicKey{ - OwnerID: ownerID, - Name: content, - Content: content, - Mode: perm.AccessModeWrite, - Type: 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) -} - // CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content string) (_ string, err error) { if setting.SSH.Disabled { diff --git a/models/auth/source.go b/models/auth/source.go index 1bdde8235c..f360ca9801 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -243,14 +244,14 @@ func CreateSource(ctx context.Context, source *Source) error { type FindSourcesOptions struct { db.ListOptions - IsActive util.OptionalBool + IsActive optional.Option[bool] LoginType Type } func (opts FindSourcesOptions) ToConds() builder.Cond { conds := builder.NewCond() - if !opts.IsActive.IsNone() { - conds = conds.And(builder.Eq{"is_active": opts.IsActive.IsTrue()}) + if opts.IsActive.Has() { + conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()}) } if opts.LoginType != NoType { conds = conds.And(builder.Eq{"`type`": opts.LoginType}) @@ -262,7 +263,7 @@ func (opts FindSourcesOptions) ToConds() builder.Cond { // source of type LoginSSPI func IsSSPIEnabled(ctx context.Context) bool { exist, err := db.Exist[Source](ctx, FindSourcesOptions{ - IsActive: util.OptionalBoolTrue, + IsActive: optional.Some(true), LoginType: SSPI, }.ToConds()) if err != nil { diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index 51061e5205..d0c341a192 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -6,6 +6,7 @@ package auth import ( "context" "crypto/md5" + "crypto/sha256" "crypto/subtle" "encoding/base32" "encoding/base64" @@ -18,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/minio/sha256-simd" "github.com/pquerna/otp/totp" "golang.org/x/crypto/pbkdf2" ) diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index bbe16483bf..9c56e0f9a0 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -24,7 +24,7 @@ import ( const ( // DefaultAvatarClass is the default class of a rendered avatar - DefaultAvatarClass = "ui avatar gt-vm" + DefaultAvatarClass = "ui avatar tw-align-middle" // DefaultAvatarPixelSize is the default size in pixels of a rendered avatar DefaultAvatarPixelSize = 28 ) diff --git a/models/db/collation.go b/models/db/collation.go index 2f5ff2bf05..c128cf5029 100644 --- a/models/db/collation.go +++ b/models/db/collation.go @@ -166,8 +166,7 @@ func preprocessDatabaseCollation(x *xorm.Engine) { // try to alter database collation to expected if the database is empty, it might fail in some cases (and it isn't necessary to succeed) // at the moment, there is no "altering" solution for MSSQL, site admin should manually change the database collation - // and there is a bug https://github.com/go-testfixtures/testfixtures/pull/182 mssql: Invalid object name 'information_schema.tables'. - if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 && x.Dialect().URI().DBType == schemas.MYSQL { + if !r.CollationEquals(r.DatabaseCollation, r.ExpectedCollation) && r.ExistingTableNumber == 0 { if err = alterDatabaseCollation(x, r.ExpectedCollation); err != nil { log.Error("Failed to change database collation to %q: %v", r.ExpectedCollation, err) } else { diff --git a/models/db/consistency.go b/models/db/consistency.go index d19732cf80..d0b0ab8315 100644 --- a/models/db/consistency.go +++ b/models/db/consistency.go @@ -10,21 +10,21 @@ import ( ) // CountOrphanedObjects count subjects with have no existing refobject anymore -func CountOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) (int64, error) { +func CountOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) (int64, error) { return GetEngine(ctx). Table("`"+subject+"`"). - Join("LEFT", "`"+refobject+"`", joinCond). - Where(builder.IsNull{"`" + refobject + "`.id"}). + Join("LEFT", "`"+refObject+"`", joinCond). + Where(builder.IsNull{"`" + refObject + "`.id"}). Select("COUNT(`" + subject + "`.`id`)"). Count() } // DeleteOrphanedObjects delete subjects with have no existing refobject anymore -func DeleteOrphanedObjects(ctx context.Context, subject, refobject, joinCond string) error { +func DeleteOrphanedObjects(ctx context.Context, subject, refObject, joinCond string) error { subQuery := builder.Select("`"+subject+"`.id"). From("`"+subject+"`"). - Join("LEFT", "`"+refobject+"`", joinCond). - Where(builder.IsNull{"`" + refobject + "`.id"}) + Join("LEFT", "`"+refObject+"`", joinCond). + Where(builder.IsNull{"`" + refObject + "`.id"}) b := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`") _, err := GetEngine(ctx).Exec(b) return err diff --git a/models/db/context.go b/models/db/context.go index cda608af19..43f612518a 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -120,6 +120,16 @@ func (c *halfCommitter) Close() error { // TxContext represents a transaction Context, // it will reuse the existing transaction in the parent context or create a new one. +// Some tips to use: +// +// 1 It's always recommended to use `WithTx` in new code instead of `TxContext`, since `WithTx` will handle the transaction automatically. +// 2. To maintain the old code which uses `TxContext`: +// a. Always call `Close()` before returning regardless of whether `Commit()` has been called. +// b. Always call `Commit()` before returning if there are no errors, even if the code did not change any data. +// c. Remember the `Committer` will be a halfCommitter when a transaction is being reused. +// So calling `Commit()` will do nothing, but calling `Close()` without calling `Commit()` will rollback the transaction. +// And all operations submitted by the caller stack will be rollbacked as well, not only the operations in the current function. +// d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback. func TxContext(parentCtx context.Context) (*Context, Committer, error) { if sess, ok := inTransaction(parentCtx); ok { return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil diff --git a/models/db/engine.go b/models/db/engine.go index 2cd1c36c58..8684c4e2f1 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -11,16 +11,19 @@ import ( "io" "reflect" "strings" + "time" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" + "xorm.io/xorm/contexts" "xorm.io/xorm/names" "xorm.io/xorm/schemas" - _ "github.com/denisenkom/go-mssqldb" // Needed for the MSSQL driver - _ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver - _ "github.com/lib/pq" // Needed for the Postgresql driver + _ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver + _ "github.com/lib/pq" // Needed for the Postgresql driver + _ "github.com/microsoft/go-mssqldb" // Needed for the MSSQL driver ) var ( @@ -143,6 +146,13 @@ func InitEngine(ctx context.Context) error { xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) xormEngine.SetDefaultContext(ctx) + if setting.Database.SlowQueryThreshold > 0 { + xormEngine.AddHook(&SlowQueryHook{ + Threshold: setting.Database.SlowQueryThreshold, + Logger: log.GetLogger("xorm"), + }) + } + SetDefaultEngine(ctx, xormEngine) return nil } @@ -298,3 +308,24 @@ func SetLogSQL(ctx context.Context, on bool) { sess.Engine().ShowSQL(on) } } + +type SlowQueryHook struct { + Threshold time.Duration + Logger log.Logger +} + +var _ contexts.Hook = &SlowQueryHook{} + +func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + return c.Ctx, nil +} + +func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { + if c.ExecuteTime >= h.Threshold { + // 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function + // is being displayed (the function that ultimately wants to execute the query in the code) + // instead of the function of the slow query hook being called. + h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime) + } + return nil +} diff --git a/models/error.go b/models/error.go index 83dfe29805..75c53245de 100644 --- a/models/error.go +++ b/models/error.go @@ -493,6 +493,23 @@ func (err ErrMergeUnrelatedHistories) Error() string { return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) } +// ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge +type ErrMergeDivergingFastForwardOnly struct { + StdOut string + StdErr string + Err error +} + +// IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly. +func IsErrMergeDivergingFastForwardOnly(err error) bool { + _, ok := err.(ErrMergeDivergingFastForwardOnly) + return ok +} + +func (err ErrMergeDivergingFastForwardOnly) Error() string { + return fmt.Sprintf("Merge DivergingFastForwardOnly Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) +} + // ErrRebaseConflicts represents an error if rebase fails with a conflict type ErrRebaseConflicts struct { Style repo_model.MergeStyle diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 1bb6a9a8ac..4171e31fef 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -42,96 +42,132 @@ - id: 8 - user_id: 15 + user_id: 10 repo_id: 21 mode: 2 - id: 9 - user_id: 15 - repo_id: 22 + user_id: 10 + repo_id: 32 mode: 2 - id: 10 user_id: 15 + repo_id: 21 + mode: 2 + +- + id: 11 + user_id: 15 + repo_id: 22 + mode: 2 + +- + id: 12 + user_id: 15 repo_id: 23 mode: 4 - - id: 11 + id: 13 user_id: 15 repo_id: 24 mode: 4 - - id: 12 + id: 14 user_id: 15 repo_id: 32 mode: 2 - - id: 13 + id: 15 user_id: 18 repo_id: 21 mode: 2 - - id: 14 + id: 16 user_id: 18 repo_id: 22 mode: 2 - - id: 15 + id: 17 user_id: 18 repo_id: 23 mode: 4 - - id: 16 + id: 18 user_id: 18 repo_id: 24 mode: 4 - - id: 17 + id: 19 user_id: 20 repo_id: 24 mode: 1 - - id: 18 + id: 20 user_id: 20 repo_id: 27 mode: 4 - - id: 19 + id: 21 user_id: 20 repo_id: 28 mode: 4 - - id: 20 + id: 22 user_id: 29 repo_id: 4 mode: 2 - - id: 21 + id: 23 user_id: 29 repo_id: 24 mode: 1 - - id: 22 + id: 24 user_id: 31 repo_id: 27 mode: 4 - - id: 23 + id: 25 user_id: 31 repo_id: 28 mode: 4 + +- + id: 26 + user_id: 38 + repo_id: 60 + mode: 2 + +- + id: 27 + user_id: 38 + repo_id: 61 + mode: 1 + +- + id: 28 + user_id: 39 + repo_id: 61 + mode: 1 + +- + id: 29 + user_id: 40 + repo_id: 61 + mode: 4 diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 2c2151f354..a42ab77ca5 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -17,3 +17,22 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 792 + title: "update actions" + repo_id: 4 + owner_id: 1 + workflow_id: "artifact.yaml" + index: 188 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 071998b979..fd90f4fd5d 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -12,3 +12,17 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 193 + run_id: 792 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 48 + status: 1 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index c78fb3c5d6..443effe08c 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -18,3 +18,23 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 48 + job_id: 193 + attempt: 1 + runner_id: 1 + status: 6 # 6 is the status code for "running", running task can upload artifacts + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: ffffcfffffffbffffffffffffffffefffffffafffffffffffffffffffffffffffffdffffffffffffffffffffffffffffffff + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml index ef77d22b24..4c3ac367f6 100644 --- a/models/fixtures/collaboration.yml +++ b/models/fixtures/collaboration.yml @@ -45,3 +45,21 @@ repo_id: 22 user_id: 18 mode: 2 # write + +- + id: 9 + repo_id: 60 + user_id: 38 + mode: 2 # write + +- + id: 10 + repo_id: 21 + user_id: 10 + mode: 2 # write + +- + id: 11 + repo_id: 32 + user_id: 10 + mode: 2 # write diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml index 17586caa21..74fc716180 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -75,3 +75,11 @@ content: "comment in private pository" created_unix: 946684811 updated_unix: 946684811 + +- + id: 9 + type: 22 # review + poster_id: 2 + issue_id: 2 # in repo_id 1 + review_id: 20 + created_unix: 946684810 diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml index 67a99f43e2..b2a0432635 100644 --- a/models/fixtures/email_address.yml +++ b/models/fixtures/email_address.yml @@ -293,3 +293,27 @@ lower_email: user37@example.com is_activated: true is_primary: true + +- + id: 38 + uid: 38 + email: user38@example.com + lower_email: user38@example.com + is_activated: true + is_primary: true + +- + id: 39 + uid: 39 + email: user39@example.com + lower_email: user39@example.com + is_activated: true + is_primary: true + +- + id: 40 + uid: 40 + email: user40@example.com + lower_email: user40@example.com + is_activated: true + is_primary: true diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index 6dbb10151a..d573406b36 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -3,3 +3,35 @@ hook_id: 1 uuid: uuid1 is_delivered: true + is_succeed: false + request_content: > + { + "url": "/matrix-delivered", + "http_method":"PUT", + "headers": { + "X-Head": "42" + }, + "body": "{}" + } + +- + id: 2 + hook_id: 1 + uuid: uuid2 + is_delivered: false + +- + id: 3 + hook_id: 1 + uuid: uuid3 + is_delivered: true + is_succeed: true + payload_content: '{"key":"value"}' # legacy task, payload saved in payload_content (and not in request_content) + request_content: > + { + "url": "/matrix-success", + "http_method":"PUT", + "headers": { + "X-Head": "42" + } + } diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml index 0c9b6ff406..ca5b1c6cd1 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -338,3 +338,37 @@ created_unix: 978307210 updated_unix: 978307210 is_locked: false + +- + id: 21 + repo_id: 60 + index: 1 + poster_id: 39 + original_author_id: 0 + name: repo60 pull1 + content: content for the 1st issue + milestone_id: 0 + priority: 0 + is_closed: false + is_pull: true + num_comments: 0 + created_unix: 1707270422 + updated_unix: 1707270422 + is_locked: false + +- + id: 22 + repo_id: 61 + index: 1 + poster_id: 40 + original_author_id: 0 + name: repo61 pull1 + content: content for the 1st issue + milestone_id: 0 + priority: 0 + is_closed: false + is_pull: true + num_comments: 0 + created_unix: 1707270422 + updated_unix: 1707270422 + is_locked: false diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml index e5d36f921a..c40ecad676 100644 --- a/models/fixtures/issue_assignees.yml +++ b/models/fixtures/issue_assignees.yml @@ -14,3 +14,7 @@ id: 4 assignee_id: 2 issue_id: 17 +- + id: 5 + assignee_id: 10 + issue_id: 6 diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml index 8d58169a32..a7fbcb2c5a 100644 --- a/models/fixtures/org_user.yml +++ b/models/fixtures/org_user.yml @@ -99,3 +99,21 @@ uid: 5 org_id: 36 is_public: true + +- + id: 18 + uid: 38 + org_id: 41 + is_public: true + +- + id: 19 + uid: 39 + org_id: 41 + is_public: true + +- + id: 20 + uid: 40 + org_id: 41 + is_public: true diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml index 1bf8030f6a..44d87bce04 100644 --- a/models/fixtures/project.yml +++ b/models/fixtures/project.yml @@ -45,3 +45,27 @@ type: 2 created_unix: 1688973000 updated_unix: 1688973000 + +- + id: 5 + title: project without default column + owner_id: 2 + repo_id: 0 + is_closed: false + creator_id: 2 + board_type: 1 + type: 2 + created_unix: 1688973000 + updated_unix: 1688973000 + +- + id: 6 + title: project with multiple default columns + owner_id: 2 + repo_id: 0 + is_closed: false + creator_id: 2 + board_type: 1 + type: 2 + created_unix: 1688973000 + updated_unix: 1688973000 diff --git a/models/fixtures/project_board.yml b/models/fixtures/project_board.yml index dc4f9cf565..3293dea6ed 100644 --- a/models/fixtures/project_board.yml +++ b/models/fixtures/project_board.yml @@ -3,6 +3,7 @@ project_id: 1 title: To Do creator_id: 2 + default: true created_unix: 1588117528 updated_unix: 1588117528 @@ -29,3 +30,48 @@ creator_id: 2 created_unix: 1588117528 updated_unix: 1588117528 + +- + id: 5 + project_id: 2 + title: Backlog + creator_id: 2 + default: true + created_unix: 1588117528 + updated_unix: 1588117528 + +- + id: 6 + project_id: 4 + title: Backlog + creator_id: 2 + default: true + created_unix: 1588117528 + updated_unix: 1588117528 + +- + id: 7 + project_id: 5 + title: Done + creator_id: 2 + default: false + created_unix: 1588117528 + updated_unix: 1588117528 + +- + id: 8 + project_id: 6 + title: Backlog + creator_id: 2 + default: true + created_unix: 1588117528 + updated_unix: 1588117528 + +- + id: 9 + project_id: 6 + title: Uncategorized + creator_id: 2 + default: true + created_unix: 1588117528 + updated_unix: 1588117528 diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml index 560674c370..3fc8ce630d 100644 --- a/models/fixtures/pull_request.yml +++ b/models/fixtures/pull_request.yml @@ -9,6 +9,7 @@ head_branch: branch1 base_branch: master merge_base: 4a357436d925b5c974181ff12a994538ddc5a269 + merged_commit_id: 1a8823cd1a9549fde083f992f6b9b87a7ab74fb3 has_merged: true merger_id: 2 @@ -98,3 +99,21 @@ index: 1 head_repo_id: 23 base_repo_id: 23 + +- + id: 9 + type: 0 # gitea pull request + status: 2 # mergable + issue_id: 21 + index: 1 + head_repo_id: 60 + base_repo_id: 60 + +- + id: 10 + type: 0 # gitea pull request + status: 2 # mergable + issue_id: 22 + index: 1 + head_repo_id: 61 + base_repo_id: 61 diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml index b841b5e983..db92c95248 100644 --- a/models/fixtures/repo_transfer.yml +++ b/models/fixtures/repo_transfer.yml @@ -5,3 +5,19 @@ repo_id: 3 created_unix: 1553610671 updated_unix: 1553610671 + +- + id: 2 + doer_id: 16 + recipient_id: 10 + repo_id: 21 + created_unix: 1553610671 + updated_unix: 1553610671 + +- + id: 3 + doer_id: 3 + recipient_id: 10 + repo_id: 32 + created_unix: 1553610671 + updated_unix: 1553610671 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index e6c59f527a..8a22db0445 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -650,12 +650,6 @@ type: 2 created_unix: 946684810 -- - id: 98 - repo_id: 1 - type: 8 - created_unix: 946684810 - - id: 99 repo_id: 1 @@ -676,3 +670,45 @@ type: 1 config: "{}" created_unix: 946684810 + +- + id: 102 + repo_id: 60 + type: 1 + config: "{}" + created_unix: 946684810 + +- + id: 103 + repo_id: 60 + type: 2 + config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}" + created_unix: 946684810 + +- + id: 104 + repo_id: 60 + type: 3 + config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" + created_unix: 946684810 + +- + id: 105 + repo_id: 61 + type: 1 + config: "{}" + created_unix: 946684810 + +- + id: 106 + repo_id: 61 + type: 2 + config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}" + created_unix: 946684810 + +- + id: 107 + repo_id: 61 + type: 3 + config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index f4e8376735..e5c6224c96 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -614,8 +614,8 @@ owner_name: user16 lower_name: big_test_public_3 name: big_test_public_3 - num_watches: 0 - num_stars: 0 + num_watches: 1 + num_stars: 1 num_forks: 0 num_issues: 0 num_closed_issues: 0 @@ -945,8 +945,8 @@ owner_name: org3 lower_name: repo21 name: repo21 - num_watches: 0 - num_stars: 0 + num_watches: 1 + num_stars: 1 num_forks: 0 num_issues: 2 num_closed_issues: 0 @@ -1706,3 +1706,65 @@ is_private: true status: 0 num_issues: 0 + +- + id: 60 + owner_id: 40 + owner_name: user40 + lower_name: repo60 + name: repo60 + default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 1 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: false + is_empty: false + is_archived: false + is_mirror: false + status: 0 + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + is_fsck_enabled: true + close_issues_via_commit_in_any_branch: false + +- + id: 61 + owner_id: 41 + owner_name: org41 + lower_name: repo61 + name: repo61 + default_branch: main + num_watches: 0 + num_stars: 0 + num_forks: 0 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 1 + num_closed_pulls: 0 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 + num_closed_projects: 0 + is_private: false + is_empty: false + is_archived: false + is_mirror: false + status: 0 + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + is_fsck_enabled: true + close_issues_via_commit_in_any_branch: false diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml index 7a88080068..ac97e24c2b 100644 --- a/models/fixtures/review.yml +++ b/models/fixtures/review.yml @@ -170,3 +170,12 @@ content: "review request for user15" updated_unix: 946684835 created_unix: 946684835 + +- + id: 20 + type: 22 + reviewer_id: 1 + issue_id: 2 + content: "Review Comment" + updated_unix: 946684810 + created_unix: 946684810 diff --git a/models/fixtures/star.yml b/models/fixtures/star.yml index 860f26b8e2..39b51b3736 100644 --- a/models/fixtures/star.yml +++ b/models/fixtures/star.yml @@ -7,3 +7,13 @@ id: 2 uid: 2 repo_id: 4 + +- + id: 3 + uid: 10 + repo_id: 21 + +- + id: 4 + uid: 10 + repo_id: 32 diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index 295e51e39c..149fe90888 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -217,3 +217,25 @@ num_members: 1 includes_all_repositories: false can_create_org_repo: true + +- + id: 21 + org_id: 41 + lower_name: owners + name: Owners + authorize: 4 # owner + num_repos: 1 + num_members: 1 + includes_all_repositories: true + can_create_org_repo: true + +- + id: 22 + org_id: 41 + lower_name: team1 + name: Team1 + authorize: 1 # read + num_repos: 1 + num_members: 2 + includes_all_repositories: false + can_create_org_repo: false diff --git a/models/fixtures/team_repo.yml b/models/fixtures/team_repo.yml index 8497720892..a29078107e 100644 --- a/models/fixtures/team_repo.yml +++ b/models/fixtures/team_repo.yml @@ -63,3 +63,15 @@ org_id: 17 team_id: 9 repo_id: 24 + +- + id: 12 + org_id: 41 + team_id: 21 + repo_id: 61 + +- + id: 13 + org_id: 41 + team_id: 22 + repo_id: 61 diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml index c5531aa57a..de0e8d738b 100644 --- a/models/fixtures/team_unit.yml +++ b/models/fixtures/team_unit.yml @@ -286,3 +286,39 @@ team_id: 2 type: 8 access_mode: 2 + +- + id: 49 + team_id: 21 + type: 1 + access_mode: 4 + +- + id: 50 + team_id: 21 + type: 2 + access_mode: 4 + +- + id: 51 + team_id: 21 + type: 3 + access_mode: 4 + +- + id: 52 + team_id: 22 + type: 1 + access_mode: 1 + +- + id: 53 + team_id: 22 + type: 2 + access_mode: 1 + +- + id: 54 + team_id: 22 + type: 3 + access_mode: 1 diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml index 9142fe609a..02d57ae644 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -129,3 +129,21 @@ org_id: 17 team_id: 9 uid: 15 + +- + id: 23 + org_id: 41 + team_id: 21 + uid: 40 + +- + id: 24 + org_id: 41 + team_id: 22 + uid: 38 + +- + id: 25 + org_id: 41 + team_id: 22 + uid: 39 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index aa0daedd85..a3de535508 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -361,7 +361,7 @@ use_custom_avatar: false num_followers: 0 num_following: 0 - num_stars: 0 + num_stars: 2 num_repos: 3 num_teams: 0 num_members: 0 @@ -1369,3 +1369,151 @@ repo_admin_change_team_access: false theme: "" keep_activity_private: false + +- + id: 38 + lower_name: user38 + name: user38 + full_name: User38 + email: user38@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: user38 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar38 + avatar_email: user38@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + +- + id: 39 + lower_name: user39 + name: user39 + full_name: User39 + email: user39@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: user39 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar39 + avatar_email: user39@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + +- + id: 40 + lower_name: user40 + name: user40 + full_name: User40 + email: user40@example.com + keep_email_private: false + email_notifications_preference: onmention + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: user40 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar40 + avatar_email: user40@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 1 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + +- + id: 41 + lower_name: org41 + name: org41 + full_name: Org41 + email: org41@example.com + keep_email_private: false + email_notifications_preference: onmention + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: org41 + type: 1 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: avatar41 + avatar_email: org41@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 1 + num_teams: 2 + num_members: 3 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false diff --git a/models/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml new file mode 100644 index 0000000000..2ec9d99df5 --- /dev/null +++ b/models/fixtures/user_blocking.yml @@ -0,0 +1,19 @@ +- + id: 1 + blocker_id: 2 + blockee_id: 29 + +- + id: 2 + blocker_id: 17 + blockee_id: 28 + +- + id: 3 + blocker_id: 2 + blockee_id: 34 + +- + id: 4 + blocker_id: 50 + blockee_id: 34 diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml index 1950ac99e7..18bcd2ed2b 100644 --- a/models/fixtures/watch.yml +++ b/models/fixtures/watch.yml @@ -27,3 +27,15 @@ user_id: 11 repo_id: 1 mode: 3 # auto + +- + id: 6 + user_id: 10 + repo_id: 21 + mode: 1 # normal + +- + id: 7 + user_id: 10 + repo_id: 32 + mode: 1 # normal diff --git a/models/git/branch.go b/models/git/branch.go index db02fc9b28..2979dff3d2 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -158,6 +158,11 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e return &branch, nil } +func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) { + branches := make([]*Branch, 0, len(branchNames)) + return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches) +} + func AddBranches(ctx context.Context, branches []*Branch) error { for _, branch := range branches { if _, err := db.GetEngine(ctx).Insert(branch); err != nil { @@ -292,6 +297,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str sess := db.GetEngine(ctx) + // check whether from branch exist var branch Branch exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch) if err != nil { @@ -303,6 +309,24 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str } } + // check whether to branch exist or is_deleted + var dstBranch Branch + exist, err = db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, to).Get(&dstBranch) + if err != nil { + return err + } + if exist { + if !dstBranch.IsDeleted { + return ErrBranchAlreadyExists{ + BranchName: to, + } + } + + if _, err := db.GetEngine(ctx).ID(dstBranch.ID).NoAutoCondition().Delete(&dstBranch); err != nil { + return err + } + } + // 1. update branch in database if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{ Name: to, @@ -357,12 +381,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str return err } - // 5. do git action - if err = gitAction(ctx, isDefault); err != nil { - return err - } - - // 6. insert renamed branch record + // 5. insert renamed branch record renamedBranch := &RenamedBranch{ RepoID: repo.ID, From: from, @@ -373,6 +392,11 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str return err } + // 6. do git action + if err = gitAction(ctx, isDefault); err != nil { + return err + } + return committer.Commit() } diff --git a/models/git/branch_list.go b/models/git/branch_list.go index 0e8d28038a..980bd7b4c9 100644 --- a/models/git/branch_list.go +++ b/models/git/branch_list.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" ) @@ -17,15 +17,12 @@ import ( type BranchList []*Branch func (branches BranchList) LoadDeletedBy(ctx context.Context) error { - ids := container.Set[int64]{} - for _, branch := range branches { - if !branch.IsDeleted { - continue - } - ids.Add(branch.DeletedByID) - } + ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) { + return branch.DeletedByID, branch.IsDeleted + }) + usersMap := make(map[int64]*user_model.User, len(ids)) - if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil { + if err := db.GetEngine(ctx).In("id", ids).Find(&usersMap); err != nil { return err } for _, branch := range branches { @@ -41,14 +38,13 @@ func (branches BranchList) LoadDeletedBy(ctx context.Context) error { } func (branches BranchList) LoadPusher(ctx context.Context) error { - ids := container.Set[int64]{} - for _, branch := range branches { - if branch.PusherID > 0 { // pusher_id maybe zero because some branches are sync by backend with no pusher - ids.Add(branch.PusherID) - } - } + ids := container.FilterSlice(branches, func(branch *Branch) (int64, bool) { + // pusher_id maybe zero because some branches are sync by backend with no pusher + return branch.PusherID, branch.PusherID > 0 + }) + usersMap := make(map[int64]*user_model.User, len(ids)) - if err := db.GetEngine(ctx).In("id", ids.Values()).Find(&usersMap); err != nil { + if err := db.GetEngine(ctx).In("id", ids).Find(&usersMap); err != nil { return err } for _, branch := range branches { @@ -67,7 +63,7 @@ type FindBranchOptions struct { db.ListOptions RepoID int64 ExcludeBranchNames []string - IsDeletedBranch util.OptionalBool + IsDeletedBranch optional.Option[bool] OrderBy string Keyword string } @@ -81,8 +77,8 @@ func (opts FindBranchOptions) ToConds() builder.Cond { if len(opts.ExcludeBranchNames) > 0 { cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames)) } - if !opts.IsDeletedBranch.IsNone() { - cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.IsTrue()}) + if opts.IsDeletedBranch.Has() { + cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.Value()}) } if opts.Keyword != "" { cond = cond.And(builder.Like{"name", opts.Keyword}) @@ -92,7 +88,7 @@ func (opts FindBranchOptions) ToConds() builder.Cond { func (opts FindBranchOptions) ToOrders() string { orderBy := opts.OrderBy - if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end + if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end if orderBy != "" { orderBy += ", " } diff --git a/models/git/branch_test.go b/models/git/branch_test.go index fd5d6519e9..b8ea663e81 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -13,7 +13,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -50,7 +50,7 @@ func TestGetDeletedBranches(t *testing.T) { branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{ ListOptions: db.ListOptionsAll, RepoID: repo.ID, - IsDeletedBranch: util.OptionalBoolTrue, + IsDeletedBranch: optional.Some(true), }) assert.NoError(t, err) assert.Len(t, branches, 2) diff --git a/models/git/commit_status.go b/models/git/commit_status.go index 5b8b323a34..0ed11cd217 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/translation" "xorm.io/builder" + "xorm.io/xorm" ) // CommitStatus holds a single Status of a single Commit @@ -195,7 +196,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string { // LocaleString returns the locale string name of the Status func (status *CommitStatus) LocaleString(lang translation.Locale) string { - return lang.Tr("repo.commitstatus." + status.State.String()) + return lang.TrString("repo.commitstatus." + status.State.String()) } // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc @@ -270,63 +271,70 @@ type CommitStatusIndex struct { // GetLatestCommitStatus returns all statuses with a unique context for a given commit. func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) { - ids := make([]int64, 0, 10) - sess := db.GetEngine(ctx).Table(&CommitStatus{}). - Where("repo_id = ?", repoID).And("sha = ?", sha). - Select("max( id ) as id"). - GroupBy("context_hash").OrderBy("max( id ) desc") + getBase := func() *xorm.Session { + return db.GetEngine(ctx).Table(&CommitStatus{}). + Where("repo_id = ?", repoID).And("sha = ?", sha) + } + indices := make([]int64, 0, 10) + sess := getBase().Select("max( `index` ) as `index`"). + GroupBy("context_hash").OrderBy("max( `index` ) desc") if !listOptions.IsListAll() { sess = db.SetSessionPagination(sess, &listOptions) } - count, err := sess.FindAndCount(&ids) + count, err := sess.FindAndCount(&indices) if err != nil { return nil, count, err } - statuses := make([]*CommitStatus, 0, len(ids)) - if len(ids) == 0 { + statuses := make([]*CommitStatus, 0, len(indices)) + if len(indices) == 0 { return statuses, count, nil } - return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses) + return statuses, count, getBase().And(builder.In("`index`", indices)).Find(&statuses) } // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs -func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) { +func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map[int64][]*CommitStatus, error) { type result struct { - ID int64 + Index int64 RepoID int64 + SHA string } - results := make([]result, 0, len(repoIDsToLatestCommitSHAs)) + results := make([]result, 0, len(repoSHAs)) - sess := db.GetEngine(ctx).Table(&CommitStatus{}) + getBase := func() *xorm.Session { + return db.GetEngine(ctx).Table(&CommitStatus{}) + } // Create a disjunction of conditions for each repoID and SHA pair - conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs)) - for repoID, sha := range repoIDsToLatestCommitSHAs { - conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha}) - } - sess = sess.Where(builder.Or(conds...)). - Select("max( id ) as id, repo_id"). - GroupBy("context_hash, repo_id").OrderBy("max( id ) desc") - - if !listOptions.IsListAll() { - sess = db.SetSessionPagination(sess, &listOptions) + conds := make([]builder.Cond, 0, len(repoSHAs)) + for _, repoSHA := range repoSHAs { + conds = append(conds, builder.Eq{"repo_id": repoSHA.RepoID, "sha": repoSHA.SHA}) } + sess := getBase().Where(builder.Or(conds...)). + Select("max( `index` ) as `index`, repo_id, sha"). + GroupBy("context_hash, repo_id, sha").OrderBy("max( `index` ) desc") err := sess.Find(&results) if err != nil { return nil, err } - ids := make([]int64, 0, len(results)) repoStatuses := make(map[int64][]*CommitStatus) - for _, result := range results { - ids = append(ids, result.ID) - } - statuses := make([]*CommitStatus, 0, len(ids)) - if len(ids) > 0 { - err = db.GetEngine(ctx).In("id", ids).Find(&statuses) + if len(results) > 0 { + statuses := make([]*CommitStatus, 0, len(results)) + + conds = make([]builder.Cond, 0, len(results)) + for _, result := range results { + cond := builder.Eq{ + "`index`": result.Index, + "repo_id": result.RepoID, + "sha": result.SHA, + } + conds = append(conds, cond) + } + err = getBase().Where(builder.Or(conds...)).Find(&statuses) if err != nil { return nil, err } @@ -343,42 +351,43 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA // GetLatestCommitStatusForRepoCommitIDs returns all statuses with a unique context for a given list of repo-sha pairs func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, commitIDs []string) (map[string][]*CommitStatus, error) { type result struct { - ID int64 - Sha string + Index int64 + SHA string } + getBase := func() *xorm.Session { + return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID) + } results := make([]result, 0, len(commitIDs)) - sess := db.GetEngine(ctx).Table(&CommitStatus{}) - - // Create a disjunction of conditions for each repoID and SHA pair conds := make([]builder.Cond, 0, len(commitIDs)) for _, sha := range commitIDs { conds = append(conds, builder.Eq{"sha": sha}) } - sess = sess.Where(builder.Eq{"repo_id": repoID}.And(builder.Or(conds...))). - Select("max( id ) as id, sha"). - GroupBy("context_hash, sha").OrderBy("max( id ) desc") + sess := getBase().And(builder.Or(conds...)). + Select("max( `index` ) as `index`, sha"). + GroupBy("context_hash, sha").OrderBy("max( `index` ) desc") err := sess.Find(&results) if err != nil { return nil, err } - ids := make([]int64, 0, len(results)) repoStatuses := make(map[string][]*CommitStatus) - for _, result := range results { - ids = append(ids, result.ID) - } - statuses := make([]*CommitStatus, 0, len(ids)) - if len(ids) > 0 { - err = db.GetEngine(ctx).In("id", ids).Find(&statuses) + if len(results) > 0 { + statuses := make([]*CommitStatus, 0, len(results)) + + conds = make([]builder.Cond, 0, len(results)) + for _, result := range results { + conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA}) + } + err = getBase().And(builder.Or(conds...)).Find(&statuses) if err != nil { return nil, err } - // Group the statuses by repo ID + // Group the statuses by commit for _, status := range statuses { repoStatuses[status.SHA] = append(repoStatuses[status.SHA], status) } @@ -389,22 +398,36 @@ func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, co // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) { + type result struct { + Index int64 + SHA string + } + getBase := func() *xorm.Session { + return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID) + } + start := timeutil.TimeStampNow().AddDuration(-before) - ids := make([]int64, 0, 10) - if err := db.GetEngine(ctx).Table("commit_status"). - Where("repo_id = ?", repoID). - And("updated_unix >= ?", start). - Select("max( id ) as id"). - GroupBy("context_hash").OrderBy("max( id ) desc"). - Find(&ids); err != nil { + results := make([]result, 0, 10) + + sess := getBase().And("updated_unix >= ?", start). + Select("max( `index` ) as `index`, sha"). + GroupBy("context_hash, sha").OrderBy("max( `index` ) desc") + + err := sess.Find(&results) + if err != nil { return nil, err } - contexts := make([]string, 0, len(ids)) - if len(ids) == 0 { + contexts := make([]string, 0, len(results)) + if len(results) == 0 { return contexts, nil } - return contexts, db.GetEngine(ctx).Select("context").Table("commit_status").In("id", ids).Find(&contexts) + + conds := make([]builder.Cond, 0, len(results)) + for _, result := range results { + conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA}) + } + return contexts, getBase().And(builder.Or(conds...)).Select("context").Find(&contexts) } // NewCommitStatusOptions holds options for creating a CommitStatus diff --git a/models/git/commit_status_summary.go b/models/git/commit_status_summary.go new file mode 100644 index 0000000000..01674e943d --- /dev/null +++ b/models/git/commit_status_summary.go @@ -0,0 +1,84 @@ +// Copyright 2024 Gitea. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + + "xorm.io/builder" +) + +// CommitStatusSummary holds the latest commit Status of a single Commit +type CommitStatusSummary struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"` + State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` +} + +func init() { + db.RegisterModel(new(CommitStatusSummary)) +} + +type RepoSHA struct { + RepoID int64 + SHA string +} + +func GetLatestCommitStatusForRepoAndSHAs(ctx context.Context, repoSHAs []RepoSHA) ([]*CommitStatus, error) { + cond := builder.NewCond() + for _, rs := range repoSHAs { + cond = cond.Or(builder.Eq{"repo_id": rs.RepoID, "sha": rs.SHA}) + } + + var summaries []CommitStatusSummary + if err := db.GetEngine(ctx).Where(cond).Find(&summaries); err != nil { + return nil, err + } + + commitStatuses := make([]*CommitStatus, 0, len(repoSHAs)) + for _, summary := range summaries { + commitStatuses = append(commitStatuses, &CommitStatus{ + RepoID: summary.RepoID, + SHA: summary.SHA, + State: summary.State, + }) + } + return commitStatuses, nil +} + +func UpdateCommitStatusSummary(ctx context.Context, repoID int64, sha string) error { + commitStatuses, _, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll) + if err != nil { + return err + } + state := CalcCommitStatus(commitStatuses) + // mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database, + // so we need to use insert in on duplicate + if setting.Database.Type.IsMySQL() { + _, err := db.GetEngine(ctx).Exec("INSERT INTO commit_status_summary (repo_id,sha,state) VALUES (?,?,?) ON DUPLICATE KEY UPDATE state=?", + repoID, sha, state.State, state.State) + return err + } + + if cnt, err := db.GetEngine(ctx).Where("repo_id=? AND sha=?", repoID, sha). + Cols("state"). + Update(&CommitStatusSummary{ + State: state.State, + }); err != nil { + return err + } else if cnt == 0 { + _, err = db.GetEngine(ctx).Insert(&CommitStatusSummary{ + RepoID: repoID, + SHA: sha, + State: state.State, + }) + return err + } + return nil +} diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index eeb307e245..613333a5a2 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -8,7 +8,7 @@ import ( "sort" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/gobwas/glob" ) @@ -56,7 +56,7 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) Page: page, }, RepoID: repoID, - IsDeletedBranch: util.OptionalBoolFalse, + IsDeletedBranch: optional.Some(false), }) if err != nil { return nil, err diff --git a/models/issues/assignees.go b/models/issues/assignees.go index 60f32d9557..30234be07a 100644 --- a/models/issues/assignees.go +++ b/models/issues/assignees.go @@ -64,6 +64,27 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID}) } +type AssignedIssuesOptions struct { + db.ListOptions + AssigneeID int64 + RepoOwnerID int64 +} + +func (opts *AssignedIssuesOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.AssigneeID != 0 { + cond = cond.And(builder.In("issue.id", builder.Select("issue_id").From("issue_assignees").Where(builder.Eq{"assignee_id": opts.AssigneeID}))) + } + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.RepoOwnerID}))) + } + return cond +} + +func GetAssignedIssues(ctx context.Context, opts *AssignedIssuesOptions) ([]*Issue, int64, error) { + return db.FindAndCount[Issue](ctx, opts) +} + // ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) { ctx, committer, err := db.TxContext(ctx) diff --git a/models/issues/comment.go b/models/issues/comment.go index c63fcab894..353163ebd6 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -8,6 +8,7 @@ package issues import ( "context" "fmt" + "html/template" "strconv" "unicode/utf8" @@ -21,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -210,12 +212,12 @@ const ( // LocaleString returns the locale string name of the role func (r RoleInRepo) LocaleString(lang translation.Locale) string { - return lang.Tr("repo.issues.role." + string(r)) + return lang.TrString("repo.issues.role." + string(r)) } // LocaleHelper returns the locale tooltip of the role func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { - return lang.Tr("repo.issues.role." + string(r) + "_helper") + return lang.TrString("repo.issues.role." + string(r) + "_helper") } // Comment represents a comment in commit and issue page. @@ -259,8 +261,8 @@ type Comment struct { CommitID int64 Line int64 // - previous line / + proposed line TreePath string - Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` + Content string `xorm:"LONGTEXT"` + RenderedContent template.HTML `xorm:"-"` // Path represents the 4 lines of code cemented by this comment Patch string `xorm:"-"` @@ -671,7 +673,8 @@ func (c *Comment) LoadTime(ctx context.Context) error { return err } -func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { +// LoadReactions loads comment reactions +func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { if c.Reactions != nil { return nil } @@ -689,14 +692,16 @@ func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository return nil } -// LoadReactions loads comment reactions -func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) error { - return c.loadReactions(ctx, repo) -} - func (c *Comment) loadReview(ctx context.Context) (err error) { + if c.ReviewID == 0 { + return nil + } if c.Review == nil { if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil { + // review request which has been replaced by actual reviews doesn't exist in database anymore, so ignorem them. + if c.Type == CommentTypeReviewRequest { + return nil + } return err } } @@ -848,6 +853,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment // Check comment type. switch opts.Type { case CommentTypeCode: + if err = updateAttachments(ctx, opts, comment); err != nil { + return err + } if comment.ReviewID != 0 { if comment.Review == nil { if err := comment.loadReview(ctx); err != nil { @@ -865,22 +873,9 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment } fallthrough case CommentTypeReview: - // Check attachments - attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) - if err != nil { - return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) + if err = updateAttachments(ctx, opts, comment); err != nil { + return err } - - for i := range attachments { - attachments[i].IssueID = opts.Issue.ID - attachments[i].CommentID = comment.ID - // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { - return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) - } - } - - comment.Attachments = attachments case CommentTypeReopen, CommentTypeClose: if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil { return err @@ -890,6 +885,23 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment return UpdateIssueCols(ctx, opts.Issue, "updated_unix") } +func updateAttachments(ctx context.Context, opts *CreateCommentOptions, comment *Comment) error { + attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err) + } + for i := range attachments { + attachments[i].IssueID = opts.Issue.ID + attachments[i].CommentID = comment.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) + } + } + comment.Attachments = attachments + return nil +} + func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) { var content string var commentType CommentType @@ -1021,8 +1033,8 @@ type FindCommentsOptions struct { TreePath string Type CommentType IssueIDs []int64 - Invalidated util.OptionalBool - IsPull util.OptionalBool + Invalidated optional.Option[bool] + IsPull optional.Option[bool] } // ToConds implements FindOptions interface @@ -1054,11 +1066,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond { if len(opts.TreePath) > 0 { cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath}) } - if !opts.Invalidated.IsNone() { - cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()}) + if opts.Invalidated.Has() { + cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()}) } - if opts.IsPull != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()}) + if opts.IsPull.Has() { + cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()}) } return cond } @@ -1067,7 +1079,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond { func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) { comments := make([]*Comment, 0, 10) sess := db.GetEngine(ctx).Where(opts.ToConds()) - if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone { + if opts.RepoID > 0 || opts.IsPull.Has() { sess.Join("INNER", "issue", "issue.id = comment.issue_id") } @@ -1260,10 +1272,9 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error { return nil } - issueIDs := make(container.Set[int64]) - for _, comment := range comments { - issueIDs.Add(comment.IssueID) - } + issueIDs := container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.IssueID, true + }) ctx, committer, err := db.TxContext(ctx) if err != nil { @@ -1286,7 +1297,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error { } } - for issueID := range issueIDs { + for _, issueID := range issueIDs { if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", issueID, CommentTypeComment, issueID); err != nil { return err diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 384a595dd9..f860dacfac 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -74,6 +74,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return nil, err } + if err := comments.LoadAttachments(ctx); err != nil { + return nil, err + } + // Find all reviews by ReviewID reviews := make(map[int64]*Review) ids := make([]int64, 0, len(comments)) @@ -122,7 +126,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number -func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) { +func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) (CommentList, error) { opts := FindCommentsOptions{ Type: CommentTypeCode, IssueID: issue.ID, diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 93af45870e..370b5396e0 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -17,11 +17,9 @@ import ( type CommentList []*Comment func (comments CommentList) getPosterIDs() []int64 { - posterIDs := make(container.Set[int64], len(comments)) - for _, comment := range comments { - posterIDs.Add(comment.PosterID) - } - return posterIDs.Values() + return container.FilterSlice(comments, func(c *Comment) (int64, bool) { + return c.PosterID, c.PosterID > 0 + }) } // LoadPosters loads posters @@ -41,20 +39,10 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { return nil } -func (comments CommentList) getCommentIDs() []int64 { - ids := make([]int64, 0, len(comments)) - for _, comment := range comments { - ids = append(ids, comment.ID) - } - return ids -} - func (comments CommentList) getLabelIDs() []int64 { - ids := make(container.Set[int64], len(comments)) - for _, comment := range comments { - ids.Add(comment.LabelID) - } - return ids.Values() + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.LabelID, comment.LabelID > 0 + }) } func (comments CommentList) loadLabels(ctx context.Context) error { @@ -98,11 +86,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error { } func (comments CommentList) getMilestoneIDs() []int64 { - ids := make(container.Set[int64], len(comments)) - for _, comment := range comments { - ids.Add(comment.MilestoneID) - } - return ids.Values() + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.MilestoneID, comment.MilestoneID > 0 + }) } func (comments CommentList) loadMilestones(ctx context.Context) error { @@ -139,11 +125,9 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { } func (comments CommentList) getOldMilestoneIDs() []int64 { - ids := make(container.Set[int64], len(comments)) - for _, comment := range comments { - ids.Add(comment.OldMilestoneID) - } - return ids.Values() + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.OldMilestoneID, comment.OldMilestoneID > 0 + }) } func (comments CommentList) loadOldMilestones(ctx context.Context) error { @@ -180,11 +164,9 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { } func (comments CommentList) getAssigneeIDs() []int64 { - ids := make(container.Set[int64], len(comments)) - for _, comment := range comments { - ids.Add(comment.AssigneeID) - } - return ids.Values() + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.AssigneeID, comment.AssigneeID > 0 + }) } func (comments CommentList) loadAssignees(ctx context.Context) error { @@ -225,20 +207,19 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { for _, comment := range comments { comment.Assignee = assignees[comment.AssigneeID] + if comment.Assignee == nil { + comment.AssigneeID = user_model.GhostUserID + comment.Assignee = user_model.NewGhostUser() + } } return nil } // getIssueIDs returns all the issue ids on this comment list which issue hasn't been loaded func (comments CommentList) getIssueIDs() []int64 { - ids := make(container.Set[int64], len(comments)) - for _, comment := range comments { - if comment.Issue != nil { - continue - } - ids.Add(comment.IssueID) - } - return ids.Values() + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.IssueID, comment.Issue == nil + }) } // Issues returns all the issues of comments @@ -305,14 +286,12 @@ func (comments CommentList) LoadIssues(ctx context.Context) error { } func (comments CommentList) getDependentIssueIDs() []int64 { - ids := make(container.Set[int64], len(comments)) - for _, comment := range comments { + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { if comment.DependentIssue != nil { - continue + return 0, false } - ids.Add(comment.DependentIssueID) - } - return ids.Values() + return comment.DependentIssueID, comment.DependentIssueID > 0 + }) } func (comments CommentList) loadDependentIssues(ctx context.Context) error { @@ -365,6 +344,35 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { return nil } +// getAttachmentCommentIDs only return the comment ids which possibly has attachments +func (comments CommentList) getAttachmentCommentIDs() []int64 { + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.ID, comment.Type.HasAttachmentSupport() + }) +} + +// LoadAttachmentsByIssue loads attachments by issue id +func (comments CommentList) LoadAttachmentsByIssue(ctx context.Context) error { + if len(comments) == 0 { + return nil + } + + attachments := make([]*repo_model.Attachment, 0, len(comments)/2) + if err := db.GetEngine(ctx).Where("issue_id=? AND comment_id>0", comments[0].IssueID).Find(&attachments); err != nil { + return err + } + + commentAttachmentsMap := make(map[int64][]*repo_model.Attachment, len(comments)) + for _, attach := range attachments { + commentAttachmentsMap[attach.CommentID] = append(commentAttachmentsMap[attach.CommentID], attach) + } + + for _, comment := range comments { + comment.Attachments = commentAttachmentsMap[comment.ID] + } + return nil +} + // LoadAttachments loads attachments func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { if len(comments) == 0 { @@ -372,16 +380,15 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { } attachments := make(map[int64][]*repo_model.Attachment, len(comments)) - commentsIDs := comments.getCommentIDs() + commentsIDs := comments.getAttachmentCommentIDs() left := len(commentsIDs) for left > 0 { limit := db.DefaultMaxInSize if left < limit { limit = left } - rows, err := db.GetEngine(ctx).Table("attachment"). - Join("INNER", "comment", "comment.id = attachment.comment_id"). - In("comment.id", commentsIDs[:limit]). + rows, err := db.GetEngine(ctx). + In("comment_id", commentsIDs[:limit]). Rows(new(repo_model.Attachment)) if err != nil { return err @@ -409,11 +416,9 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { } func (comments CommentList) getReviewIDs() []int64 { - ids := make(container.Set[int64], len(comments)) - for _, comment := range comments { - ids.Add(comment.ReviewID) - } - return ids.Values() + return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { + return comment.ReviewID, comment.ReviewID > 0 + }) } func (comments CommentList) loadReviews(ctx context.Context) error { @@ -430,7 +435,8 @@ func (comments CommentList) loadReviews(ctx context.Context) error { for _, comment := range comments { comment.Review = reviews[comment.ReviewID] if comment.Review == nil { - if comment.ReviewID > 0 { + // review request which has been replaced by actual reviews doesn't exist in database anymore, so don't log errors for them. + if comment.ReviewID > 0 && comment.Type != CommentTypeReviewRequest { log.Error("comment with review id [%d] but has no review record", comment.ReviewID) } continue diff --git a/models/issues/content_history.go b/models/issues/content_history.go index 8c333bc6dd..31c80d2cea 100644 --- a/models/issues/content_history.go +++ b/models/issues/content_history.go @@ -161,20 +161,20 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6 } for _, item := range res { - item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0) + if item.UserID > 0 { + item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0) + } else { + item.UserAvatarLink = avatars.DefaultAvatarLink() + } } return res, nil } // HasIssueContentHistory check if a ContentHistory entry exists func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) { - exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{ - IssueID: issueID, - CommentID: commentID, - }) + exists, err := db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).Exist(&ContentHistory{}) if err != nil { - log.Error("can not fetch issue content history. err=%v", err) - return false, err + return false, fmt.Errorf("can not check issue content history. err: %w", err) } return exists, err } diff --git a/models/issues/content_history_test.go b/models/issues/content_history_test.go index 0ea1d0f7b2..1caa73a948 100644 --- a/models/issues/content_history_test.go +++ b/models/issues/content_history_test.go @@ -78,3 +78,22 @@ func TestContentHistory(t *testing.T) { assert.EqualValues(t, 7, list2[1].HistoryID) assert.EqualValues(t, 4, list2[2].HistoryID) } + +func TestHasIssueContentHistoryForCommentOnly(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + _ = db.TruncateBeans(db.DefaultContext, &issues_model.ContentHistory{}) + + hasHistory1, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0) + assert.False(t, hasHistory1) + hasHistory2, _ := issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100) + assert.False(t, hasHistory2) + + _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow(), "c-a", true) + _ = issues_model.SaveIssueContentHistory(db.DefaultContext, 1, 10, 100, timeutil.TimeStampNow().Add(5), "c-b", false) + + hasHistory1, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 0) + assert.False(t, hasHistory1) + hasHistory2, _ = issues_model.HasIssueContentHistory(db.DefaultContext, 10, 100) + assert.True(t, hasHistory2) +} diff --git a/models/issues/issue.go b/models/issues/issue.go index 90aad10bb9..87c1c86eb1 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -7,6 +7,7 @@ package issues import ( "context" "fmt" + "html/template" "regexp" "slices" @@ -105,7 +106,7 @@ type Issue struct { OriginalAuthorID int64 `xorm:"index"` Title string `xorm:"name"` Content string `xorm:"LONGTEXT"` - RenderedContent string `xorm:"-"` + RenderedContent template.HTML `xorm:"-"` Labels []*Label `xorm:"-"` MilestoneID int64 `xorm:"INDEX"` Milestone *Milestone `xorm:"-"` @@ -192,20 +193,6 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool { return issue.Repo.IsTimetrackerEnabled(ctx) } -// GetPullRequest returns the issue pull request -func (issue *Issue) GetPullRequest(ctx context.Context) (pr *PullRequest, err error) { - if !issue.IsPull { - return nil, fmt.Errorf("Issue is not a pull request") - } - - pr, err = GetPullRequestByIssueID(ctx, issue.ID) - if err != nil { - return nil, err - } - pr.Issue = issue - return pr, err -} - // LoadPoster loads poster func (issue *Issue) LoadPoster(ctx context.Context) (err error) { if issue.Poster == nil && issue.PosterID != 0 { diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index a932ac2554..f8ee271a6b 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -21,16 +21,15 @@ type IssueList []*Issue // get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo func (issues IssueList) getRepoIDs() []int64 { - repoIDs := make(container.Set[int64], len(issues)) - for _, issue := range issues { + return container.FilterSlice(issues, func(issue *Issue) (int64, bool) { if issue.Repo == nil { - repoIDs.Add(issue.RepoID) + return issue.RepoID, true } if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil { - repoIDs.Add(issue.PullRequest.HeadRepoID) + return issue.PullRequest.HeadRepoID, true } - } - return repoIDs.Values() + return 0, false + }) } // LoadRepositories loads issues' all repositories @@ -74,11 +73,9 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi } func (issues IssueList) getPosterIDs() []int64 { - posterIDs := make(container.Set[int64], len(issues)) - for _, issue := range issues { - posterIDs.Add(issue.PosterID) - } - return posterIDs.Values() + return container.FilterSlice(issues, func(issue *Issue) (int64, bool) { + return issue.PosterID, true + }) } func (issues IssueList) loadPosters(ctx context.Context) error { @@ -193,11 +190,9 @@ func (issues IssueList) loadLabels(ctx context.Context) error { } func (issues IssueList) getMilestoneIDs() []int64 { - ids := make(container.Set[int64], len(issues)) - for _, issue := range issues { - ids.Add(issue.MilestoneID) - } - return ids.Values() + return container.FilterSlice(issues, func(issue *Issue) (int64, bool) { + return issue.MilestoneID, true + }) } func (issues IssueList) loadMilestones(ctx context.Context) error { @@ -370,6 +365,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error { for _, issue := range issues { issue.PullRequest = pullRequestMaps[issue.ID] + if issue.PullRequest != nil { + issue.PullRequest.Issue = issue + } } return nil } @@ -388,9 +386,8 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) { if left < limit { limit = left } - rows, err := db.GetEngine(ctx).Table("attachment"). - Join("INNER", "issue", "issue.id = attachment.issue_id"). - In("issue.id", issuesIDs[:limit]). + rows, err := db.GetEngine(ctx). + In("issue_id", issuesIDs[:limit]). Rows(new(repo_model.Attachment)) if err != nil { return err @@ -476,6 +473,16 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { } trackedTimes := make(map[int64]int64, len(issues)) + reposMap := make(map[int64]*repo_model.Repository, len(issues)) + for _, issue := range issues { + reposMap[issue.RepoID] = issue.Repo + } + repos := repo_model.RepositoryListOfMap(reposMap) + + if err := repos.LoadUnits(ctx); err != nil { + return err + } + ids := make([]int64, 0, len(issues)) for _, issue := range issues { if issue.Repo.IsTimetrackerEnabled(ctx) { @@ -599,3 +606,23 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev return approvalCountMap, nil } + +func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error { + issueIDs := issues.getIssueIDs() + issueUsers := make([]*IssueUser, 0, len(issueIDs)) + if err := db.GetEngine(ctx).Where("uid =?", userID). + In("issue_id"). + Find(&issueUsers); err != nil { + return err + } + + for _, issueUser := range issueUsers { + for _, issue := range issues { + if issue.ID == issueUser.IssueID { + issue.IsRead = issueUser.IsRead + } + } + } + + return nil +} diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go index cc7ffb356a..907a5a17b9 100644 --- a/models/issues/issue_project.go +++ b/models/issues/issue_project.go @@ -49,18 +49,13 @@ func (issue *Issue) ProjectBoardID(ctx context.Context) int64 { // LoadIssuesFromBoard load issues assigned to this board func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { - issueList := make(IssueList, 0, 10) - - if b.ID > 0 { - issues, err := Issues(ctx, &IssuesOptions{ - ProjectBoardID: b.ID, - ProjectID: b.ProjectID, - SortType: "project-column-sorting", - }) - if err != nil { - return nil, err - } - issueList = issues + issueList, err := Issues(ctx, &IssuesOptions{ + ProjectBoardID: b.ID, + ProjectID: b.ProjectID, + SortType: "project-column-sorting", + }) + if err != nil { + return nil, err } if b.Default { diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 7dc277327a..921dd9973e 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -13,7 +13,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/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" "xorm.io/xorm" @@ -21,7 +21,7 @@ import ( // IssuesOptions represents options of an issue. type IssuesOptions struct { //nolint - db.Paginator + Paginator *db.ListOptions RepoIDs []int64 // overwrites RepoCond if the length is not 0 AllPublic bool // include also all public repositories RepoCond builder.Cond @@ -34,8 +34,8 @@ type IssuesOptions struct { //nolint MilestoneIDs []int64 ProjectID int64 ProjectBoardID int64 - IsClosed util.OptionalBool - IsPull util.OptionalBool + IsClosed optional.Option[bool] + IsPull optional.Option[bool] LabelIDs []int64 IncludedLabelNames []string ExcludedLabelNames []string @@ -46,7 +46,7 @@ type IssuesOptions struct { //nolint UpdatedBeforeUnix int64 // prioritize issues from this repo PriorityRepoID int64 - IsArchived util.OptionalBool + IsArchived optional.Option[bool] Org *organization.Organization // issues permission scope Team *organization.Team // issues permission scope User *user_model.User // issues permission scope @@ -104,23 +104,11 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { return sess } - // Warning: Do not use GetSkipTake() for *db.ListOptions - // Its implementation could reset the page size with setting.API.MaxResponseItems - if listOptions, ok := opts.Paginator.(*db.ListOptions); ok { - if listOptions.Page >= 0 && listOptions.PageSize > 0 { - var start int - if listOptions.Page == 0 { - start = 0 - } else { - start = (listOptions.Page - 1) * listOptions.PageSize - } - sess.Limit(listOptions.PageSize, start) - } - return sess + start := 0 + if opts.Paginator.Page > 1 { + start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize } - - start, limit := opts.Paginator.GetSkipTake() - sess.Limit(limit, start) + sess.Limit(opts.Paginator.PageSize, start) return sess } @@ -217,8 +205,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { applyRepoConditions(sess, opts) - if !opts.IsClosed.IsNone() { - sess.And("issue.is_closed=?", opts.IsClosed.IsTrue()) + if opts.IsClosed.Has() { + sess.And("issue.is_closed=?", opts.IsClosed.Value()) } if opts.AssigneeID > 0 { @@ -260,21 +248,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { applyProjectBoardCondition(sess, opts) - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) + if opts.IsPull.Has() { + sess.And("issue.is_pull=?", opts.IsPull.Value()) } - if opts.IsArchived != util.OptionalBoolNone { - sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.IsTrue()}) + if opts.IsArchived.Has() { + sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()}) } applyLabelsCondition(sess, opts) if opts.User != nil { - sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.IsTrue())) + sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value())) } return sess @@ -396,7 +381,7 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session { // Query for pull requests where you are a reviewer or commenter, excluding - // any pull requests already returned by the the review requested filter. + // any pull requests already returned by the review requested filter. notPoster := builder.Neq{"issue.poster_id": reviewedID} reviewed := builder.In("issue.id", builder. Select("issue_id"). diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 99ca19f804..39326616f8 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -8,7 +8,6 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm" @@ -69,13 +68,17 @@ func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int6 } // CountIssues number return of issues by given conditions. -func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) { +func CountIssues(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) (int64, error) { sess := db.GetEngine(ctx). Select("COUNT(issue.id) AS count"). Table("issue"). Join("INNER", "repository", "`issue`.repo_id = `repository`.id") applyConditions(sess, opts) + for _, cond := range otherConds { + sess.And(cond) + } + return sess.Count() } @@ -170,11 +173,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6 applyReviewedCondition(sess, opts.ReviewedID) } - switch opts.IsPull { - case util.OptionalBoolTrue: - sess.And("issue.is_pull=?", true) - case util.OptionalBoolFalse: - sess.And("issue.is_pull=?", false) + if opts.IsPull.Has() { + sess.And("issue.is_pull=?", opts.IsPull.Value()) } return sess diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index cc363d2fae..1bbc0eee56 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -379,7 +379,7 @@ func TestCountIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{}) assert.NoError(t, err) - assert.EqualValues(t, 20, count) + assert.EqualValues(t, 22, count) } func TestIssueLoadAttributes(t *testing.T) { diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index b258dc882c..ef96e1ee50 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -517,6 +517,15 @@ func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_mo if err != nil { return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) } + + notBlocked := make([]*user_model.User, 0, len(mentions)) + for _, user := range mentions { + if !user_model.IsUserBlockedBy(ctx, doer, user.ID) { + notBlocked = append(notBlocked, user) + } + } + mentions = notBlocked + if err = UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { return nil, fmt.Errorf("UpdateIssueMentions [%d]: %w", issue.ID, err) } diff --git a/models/issues/issue_xref.go b/models/issues/issue_xref.go index 77ef53a013..e2e35859df 100644 --- a/models/issues/issue_xref.go +++ b/models/issues/issue_xref.go @@ -46,10 +46,10 @@ func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error for i, c := range active { ids[i] = c.ID } - return neuterCrossReferencesIds(ctx, ids) + return neuterCrossReferencesIDs(ctx, ids) } -func neuterCrossReferencesIds(ctx context.Context, ids []int64) error { +func neuterCrossReferencesIDs(ctx context.Context, ids []int64) error { _, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered}) return err } @@ -100,7 +100,7 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe } } if len(ids) > 0 { - if err = neuterCrossReferencesIds(stdCtx, ids); err != nil { + if err = neuterCrossReferencesIDs(stdCtx, ids); err != nil { return err } } @@ -214,6 +214,10 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe if !perm.CanReadIssuesOrPulls(refIssue.IsPull) { return nil, references.XRefActionNone, nil } + if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) { + return nil, references.XRefActionNone, nil + } + // Accept close/reopening actions only if the poster is able to close the // referenced issue manually at this moment. The only exception is // the poster of a new PR referencing an issue on the same repo: then the merger diff --git a/models/issues/label.go b/models/issues/label.go index 527d8d7853..2397a29e35 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/label" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -115,18 +116,23 @@ func (l *Label) CalOpenIssues() { func (l *Label) SetArchived(isArchived bool) { if !isArchived { l.ArchivedUnix = timeutil.TimeStamp(0) - } else if isArchived && l.ArchivedUnix.IsZero() { + } else if isArchived && !l.IsArchived() { // Only change the date when it is newly archived. l.ArchivedUnix = timeutil.TimeStampNow() } } +// IsArchived returns true if label is an archived +func (l *Label) IsArchived() bool { + return !l.ArchivedUnix.IsZero() +} + // CalOpenOrgIssues calculates the open issues of a label for a specific repo func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ RepoIDs: []int64{repoID}, LabelIDs: []int64{labelID}, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) for _, count := range counts { @@ -165,11 +171,6 @@ func (l *Label) BelongsToOrg() bool { return l.OrgID > 0 } -// IsArchived returns true if label is an archived -func (l *Label) IsArchived() bool { - return l.ArchivedUnix > 0 -} - // BelongsToRepo returns true if label is a repository label func (l *Label) BelongsToRepo() bool { return l.RepoID > 0 diff --git a/models/issues/milestone.go b/models/issues/milestone.go index f663d42fe9..db0312adf0 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -6,10 +6,12 @@ package issues import ( "context" "fmt" + "html/template" "strings" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "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" @@ -47,8 +49,8 @@ type Milestone struct { RepoID int64 `xorm:"INDEX"` Repo *repo_model.Repository `xorm:"-"` Name string - Content string `xorm:"TEXT"` - RenderedContent string `xorm:"-"` + Content string `xorm:"TEXT"` + RenderedContent template.HTML `xorm:"-"` IsClosed bool NumIssues int NumClosedIssues int @@ -301,7 +303,7 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error { } numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{ RepoID: repo.ID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) if err != nil { return err diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go index a73bf73c17..d1b3f0301b 100644 --- a/models/issues/milestone_list.go +++ b/models/issues/milestone_list.go @@ -8,7 +8,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" ) @@ -28,7 +28,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 { type FindMilestoneOptions struct { db.ListOptions RepoID int64 - IsClosed util.OptionalBool + IsClosed optional.Option[bool] Name string SortType string RepoCond builder.Cond @@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond { if opts.RepoID != 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.IsClosed != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()}) + if opts.IsClosed.Has() { + cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()}) } if opts.RepoCond != nil && opts.RepoCond.IsValid() { cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond))) diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index 7477af92c8..e5f6f15ca2 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -11,10 +11,10 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/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" "github.com/stretchr/testify/assert" ) @@ -39,10 +39,10 @@ func TestGetMilestoneByRepoID(t *testing.T) { func TestGetMilestonesByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64, state api.StateType) { - 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) } repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ @@ -84,7 +84,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: unittest.NonexistentID, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) assert.NoError(t, err) assert.Len(t, milestones, 0) @@ -101,7 +101,7 @@ func TestGetMilestones(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoID: repo.ID, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), SortType: sortType, }) assert.NoError(t, err) @@ -118,7 +118,7 @@ func TestGetMilestones(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoID: repo.ID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), Name: "", SortType: sortType, }) @@ -178,7 +178,7 @@ func TestCountRepoClosedMilestones(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: repoID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) assert.NoError(t, err) assert.EqualValues(t, repo.NumClosedMilestones, count) @@ -189,7 +189,7 @@ func TestCountRepoClosedMilestones(t *testing.T) { count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{ RepoID: unittest.NonexistentID, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) assert.NoError(t, err) assert.EqualValues(t, 0, count) @@ -206,7 +206,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{ RepoIDs: []int64{1, 2}, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }) assert.NoError(t, err) assert.EqualValues(t, repo1OpenCount, openCounts[1]) @@ -215,7 +215,7 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{ RepoIDs: []int64{1, 2}, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }) assert.NoError(t, err) assert.EqualValues(t, repo1ClosedCount, closedCounts[1]) @@ -234,7 +234,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoIDs: []int64{repo1.ID, repo2.ID}, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), SortType: sortType, }) assert.NoError(t, err) @@ -252,7 +252,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) { PageSize: setting.UI.IssuePagingNum, }, RepoIDs: []int64{repo1.ID, repo2.ID}, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), SortType: sortType, }) assert.NoError(t, err) diff --git a/models/issues/pull.go b/models/issues/pull.go index 2cb1e1b971..dc1b1b956a 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -19,7 +19,6 @@ import ( 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/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -652,6 +651,35 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, return pr, pr.LoadAttributes(ctx) } +// GetPullRequestsByBaseHeadInfo returns the pull request by given base and head +func GetPullRequestByBaseHeadInfo(ctx context.Context, baseID, headID int64, base, head string) (*PullRequest, error) { + pr := &PullRequest{} + sess := db.GetEngine(ctx). + Join("INNER", "issue", "issue.id = pull_request.issue_id"). + Where("base_repo_id = ? AND base_branch = ? AND head_repo_id = ? AND head_branch = ?", baseID, base, headID, head) + has, err := sess.Get(pr) + if err != nil { + return nil, err + } + if !has { + return nil, ErrPullRequestNotExist{ + HeadRepoID: headID, + BaseRepoID: baseID, + HeadBranch: head, + BaseBranch: base, + } + } + + if err = pr.LoadAttributes(ctx); err != nil { + return nil, err + } + if err = pr.LoadIssue(ctx); err != nil { + return nil, err + } + + return pr, nil +} + // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request // By poster id. func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) { @@ -855,82 +883,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr * return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 } -func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error { - files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} - - if pr.IsWorkInProgress(ctx) { - return nil - } - - if err := pr.LoadBaseRepo(ctx); err != nil { - return err - } - - repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) - if err != nil { - return err - } - defer repo.Close() - - branch, err := repo.GetDefaultBranch() - if err != nil { - return err - } - - commit, err := repo.GetBranchCommit(branch) - if err != nil { - return 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, _ := GetCodeOwnersFromContent(ctx, data) - changedFiles, err := repo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) - if err != nil { - return 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 - } - } - } - } - - for _, u := range uniqUsers { - if u.ID != pull.Poster.ID { - if _, err := AddReviewRequest(ctx, pull, u, pull.Poster); 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 err - } - } - } - for _, t := range uniqTeams { - if _, err := AddTeamReviewRequest(ctx, pull, t, pull.Poster); 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 err - } - } - - return nil -} - // GetCodeOwnersFromContent returns the code owners configuration // Return empty slice if files missing // Return warning messages on parsing errors @@ -1093,3 +1045,23 @@ func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error { } return committer.Commit() } + +// GetPullRequestByMergedCommit returns a merged pull request by the given commit +func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string) (*PullRequest, error) { + pr := new(PullRequest) + has, err := db.GetEngine(ctx).Where("base_repo_id = ? AND merged_commit_id = ?", repoID, sha).Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""} + } + + if err = pr.LoadAttributes(ctx); err != nil { + return nil, err + } + if err = pr.LoadIssue(ctx); err != nil { + return nil, err + } + + return pr, nil +} diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index c209386e2e..de3eceed37 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -11,7 +11,6 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" "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/log" "code.gitea.io/gitea/modules/util" @@ -23,7 +22,7 @@ type PullRequestsOptions struct { db.ListOptions State string SortType string - Labels []string + Labels []int64 MilestoneID int64 } @@ -36,11 +35,9 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR sess.And("issue.is_closed=?", opts.State == "closed") } - if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil { - return nil, err - } else if len(labelIDs) > 0 { + if len(opts.Labels) > 0 { sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id"). - In("issue_label.label_id", labelIDs) + In("issue_label.label_id", opts.Labels) } if opts.MilestoneID > 0 { @@ -212,3 +209,12 @@ func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bo Limit(1). Get(new(Issue)) } + +// GetPullRequestByIssueIDs returns all pull requests by issue ids +func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) { + prs := make([]*PullRequest, 0, len(issueIDs)) + return prs, db.GetEngine(ctx). + Where("issue_id > 0"). + In("issue_id", issueIDs). + Find(&prs) +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 173417136c..675c90527d 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -66,7 +66,6 @@ func TestPullRequestsNewest(t *testing.T) { }, State: "open", SortType: "newest", - Labels: []string{}, }) assert.NoError(t, err) assert.EqualValues(t, 3, count) @@ -113,7 +112,6 @@ func TestPullRequestsOldest(t *testing.T) { }, State: "open", SortType: "oldest", - Labels: []string{}, }) assert.NoError(t, err) assert.EqualValues(t, 3, count) @@ -339,6 +337,18 @@ func TestGetApprovers(t *testing.T) { assert.EqualValues(t, expected, approvers) } +func TestGetPullRequestByMergedCommit(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr, err := issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") + assert.NoError(t, err) + assert.EqualValues(t, 1, pr.ID) + + _, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 0, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") + assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) + _, err = issues_model.GetPullRequestByMergedCommit(db.DefaultContext, 1, "") + assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) +} + func TestMigrate_InsertPullRequests(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) reponame := "repo1" diff --git a/models/issues/reaction.go b/models/issues/reaction.go index bb47cf24ca..eb7faefc79 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -240,25 +240,6 @@ func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, erro return reaction, nil } -// CreateIssueReaction creates a reaction on issue. -func CreateIssueReaction(ctx context.Context, doerID, issueID int64, content string) (*Reaction, error) { - return CreateReaction(ctx, &ReactionOptions{ - Type: content, - DoerID: doerID, - IssueID: issueID, - }) -} - -// CreateCommentReaction creates a reaction on comment. -func CreateCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) (*Reaction, error) { - return CreateReaction(ctx, &ReactionOptions{ - Type: content, - DoerID: doerID, - IssueID: issueID, - CommentID: commentID, - }) -} - // DeleteReaction deletes reaction for issue or comment. func DeleteReaction(ctx context.Context, opts *ReactionOptions) error { reaction := &Reaction{ @@ -324,14 +305,12 @@ func (list ReactionList) GroupByType() map[string]ReactionList { } func (list ReactionList) getUserIDs() []int64 { - userIDs := make(container.Set[int64], len(list)) - for _, reaction := range list { + return container.FilterSlice(list, func(reaction *Reaction) (int64, bool) { if reaction.OriginalAuthor != "" { - continue + return 0, false } - userIDs.Add(reaction.UserID) - } - return userIDs.Values() + return reaction.UserID, true + }) } func valuesUser(m map[int64]*user_model.User) []*user_model.User { diff --git a/models/issues/review.go b/models/issues/review.go index f2022ae0aa..92764db4d1 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -66,6 +66,23 @@ func (err ErrNotValidReviewRequest) Unwrap() error { return util.ErrInvalidArgument } +// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR. +type ErrReviewRequestOnClosedPR struct{} + +// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR. +func IsErrReviewRequestOnClosedPR(err error) bool { + _, ok := err.(ErrReviewRequestOnClosedPR) + return ok +} + +func (err ErrReviewRequestOnClosedPR) Error() string { + return "cannot request a re-review on a closed or merged PR" +} + +func (err ErrReviewRequestOnClosedPR) Unwrap() error { + return util.ErrPermissionDenied +} + // ReviewType defines the sort of feedback a review gives type ReviewType int @@ -159,6 +176,14 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) { return err } r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) + if err != nil { + if !user_model.IsErrUserNotExist(err) { + return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err) + } + r.ReviewerID = user_model.GhostUserID + r.Reviewer = user_model.NewGhostUser() + return nil + } return err } @@ -231,11 +256,11 @@ type CreateReviewOptions struct { // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals) func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) { - pr, err := GetPullRequestByIssueID(ctx, issue.ID) - if err != nil { + if err := issue.LoadPullRequest(ctx); err != nil { return false, err } + pr := issue.PullRequest rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { return false, err @@ -263,11 +288,10 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model. // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals) func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) { - pr, err := GetPullRequestByIssueID(ctx, issue.ID) - if err != nil { + if err := issue.LoadPullRequest(ctx); err != nil { return false, err } - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, issue.PullRequest.BaseRepoID, issue.PullRequest.BaseBranch) if err != nil { return false, err } @@ -284,8 +308,14 @@ func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organizatio // CreateReview creates a new review based on opts func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return nil, err + } + defer committer.Close() + sess := db.GetEngine(ctx) + review := &Review{ - Type: opts.Type, Issue: opts.Issue, IssueID: opts.Issue.ID, Reviewer: opts.Reviewer, @@ -295,15 +325,39 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error CommitID: opts.CommitID, Stale: opts.Stale, } + if opts.Reviewer != nil { + review.Type = opts.Type review.ReviewerID = opts.Reviewer.ID - } else { - if review.Type != ReviewTypeRequest { - review.Type = ReviewTypeRequest + + reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID} + // make sure user review requests are cleared + if opts.Type != ReviewTypePending { + if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil { + return nil, err + } } + // make sure if the created review gets dismissed no old review surface + // other types can be ignored, as they don't affect branch protection + if opts.Type == ReviewTypeApprove || opts.Type == ReviewTypeReject { + if _, err := sess.Where(reviewCond.And(builder.In("type", ReviewTypeApprove, ReviewTypeReject))). + Cols("dismissed").Update(&Review{Dismissed: true}); err != nil { + return nil, err + } + } + + } else if opts.ReviewerTeam != nil { + review.Type = ReviewTypeRequest review.ReviewerTeamID = opts.ReviewerTeam.ID + + } else { + return nil, fmt.Errorf("provide either reviewer or reviewer team") } - return review, db.Insert(ctx, review) + + if _, err := sess.Insert(review); err != nil { + return nil, err + } + return review, committer.Commit() } // GetCurrentReview returns the current pending review of reviewer for given issue @@ -581,9 +635,24 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo return nil, err } - // skip it when reviewer hase been request to review - if review != nil && review.Type == ReviewTypeRequest { - return nil, nil + if review != nil { + // skip it when reviewer hase been request to review + if review.Type == ReviewTypeRequest { + return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction. + } + + if issue.IsClosed { + return nil, ErrReviewRequestOnClosedPR{} + } + + if issue.IsPull { + if err := issue.LoadPullRequest(ctx); err != nil { + return nil, err + } + if issue.PullRequest.HasMerged { + return nil, ErrReviewRequestOnClosedPR{} + } + } } // if the reviewer is an official reviewer, @@ -621,6 +690,9 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo return nil, err } + // func caller use the created comment to retrieve created review too. + comment.Review = review + return comment, committer.Commit() } diff --git a/models/issues/review_list.go b/models/issues/review_list.go index ed3d0bd028..7b8c3d319c 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "xorm.io/builder" ) @@ -18,11 +18,11 @@ type ReviewList []*Review // LoadReviewers loads reviewers func (reviews ReviewList) LoadReviewers(ctx context.Context) error { - reviewerIds := make([]int64, len(reviews)) + reviewerIDs := make([]int64, len(reviews)) for i := 0; i < len(reviews); i++ { - reviewerIds[i] = reviews[i].ReviewerID + reviewerIDs[i] = reviews[i].ReviewerID } - reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIds) + reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs) if err != nil { return err } @@ -38,12 +38,11 @@ func (reviews ReviewList) LoadReviewers(ctx context.Context) error { } func (reviews ReviewList) LoadIssues(ctx context.Context) error { - issueIds := container.Set[int64]{} - for i := 0; i < len(reviews); i++ { - issueIds.Add(reviews[i].IssueID) - } + issueIDs := container.FilterSlice(reviews, func(review *Review) (int64, bool) { + return review.IssueID, true + }) - issues, err := GetIssuesByIDs(ctx, issueIds.Values()) + issues, err := GetIssuesByIDs(ctx, issueIDs) if err != nil { return err } @@ -68,7 +67,7 @@ type FindReviewOptions struct { IssueID int64 ReviewerID int64 OfficialOnly bool - Dismissed util.OptionalBool + Dismissed optional.Option[bool] } func (opts *FindReviewOptions) toCond() builder.Cond { @@ -85,8 +84,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond { if opts.OfficialOnly { cond = cond.And(builder.Eq{"official": true}) } - if !opts.Dismissed.IsNone() { - cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.IsTrue()}) + if opts.Dismissed.Has() { + cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()}) } return cond } diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 1868cb1bfa..ac1b84adeb 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -288,3 +288,33 @@ func TestDeleteDismissedReview(t *testing.T) { assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review)) unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID}) } + +func TestAddReviewRequest(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + 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}) + _, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypeReject, + }) + + assert.NoError(t, err) + pull.HasMerged = false + assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged")) + issue.IsClosed = true + _, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{}) + assert.Error(t, err) + assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) + + pull.HasMerged = true + assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged")) + issue.IsClosed = false + _, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{}) + assert.Error(t, err) + assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) +} diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index 91c4832e49..4063ca043b 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -340,7 +341,7 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) { } // GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions. -func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool) (int64, error) { +func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool]) (int64, error) { if len(opts.IssueIDs) <= MaxQueryParameters { return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs) } @@ -363,7 +364,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed return accum, nil } -func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed util.OptionalBool, issueIDs []int64) (int64, error) { +func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool], issueIDs []int64) (int64, error) { sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session { sess := db.GetEngine(ctx). Table("tracked_time"). @@ -378,8 +379,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC } session := sumSession(opts, issueIDs) - if !isClosed.IsNone() { - session = session.And("issue.is_closed = ?", isClosed.IsTrue()) + if isClosed.Has() { + session = session.And("issue.is_closed = ?", isClosed.Value()) } return session.SumInt(new(trackedTime), "tracked_time.time") } diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index 9beb862ffb..d82bff967a 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -11,7 +11,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -120,15 +120,15 @@ func TestTotalTimesForEachUser(t *testing.T) { func TestGetIssueTotalTrackedTime(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolFalse) + ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(false)) assert.NoError(t, err) assert.EqualValues(t, 3682, ttt) - ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolTrue) + ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.Some(true)) assert.NoError(t, err) assert.EqualValues(t, 0, ttt) - ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, util.OptionalBoolNone) + ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, optional.None[bool]()) assert.NoError(t, err) assert.EqualValues(t, 3682, ttt) } diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go index 4d61b758cf..80bf00b22a 100644 --- a/models/migrations/base/db_test.go +++ b/models/migrations/base/db_test.go @@ -36,12 +36,14 @@ func Test_DropTableColumns(t *testing.T) { "updated_unix", } + x.SetMapper(names.GonicMapper{}) + for i := range columns { - x.SetMapper(names.GonicMapper{}) if err := x.Sync(new(DropTest)); err != nil { t.Errorf("unable to create DropTest table: %v", err) return } + sess := x.NewSession() if err := sess.Begin(); err != nil { sess.Close() @@ -64,7 +66,6 @@ func Test_DropTableColumns(t *testing.T) { return } for j := range columns[i+1:] { - x.SetMapper(names.GonicMapper{}) if err := x.Sync(new(DropTest)); err != nil { t.Errorf("unable to create DropTest table: %v", err) return diff --git a/models/migrations/base/hash.go b/models/migrations/base/hash.go index 0debec272b..00fd1efd4a 100644 --- a/models/migrations/base/hash.go +++ b/models/migrations/base/hash.go @@ -4,9 +4,9 @@ package base import ( + "crypto/sha256" "encoding/hex" - "github.com/minio/sha256-simd" "golang.org/x/crypto/pbkdf2" ) diff --git a/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml new file mode 100644 index 0000000000..f95d47916b --- /dev/null +++ b/models/migrations/fixtures/Test_AddIssueResourceIndexTable/issue.yml @@ -0,0 +1,4 @@ +- + id: 1 + repo_id: 1 + index: 1 diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml new file mode 100644 index 0000000000..056236ba9e --- /dev/null +++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/attachment.yml @@ -0,0 +1,11 @@ +- + id: 1 + uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 + issue_id: 1 + release_id: 0 + +- + id: 2 + uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12 + issue_id: 0 + release_id: 1 diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml new file mode 100644 index 0000000000..7f3255096d --- /dev/null +++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/issue.yml @@ -0,0 +1,3 @@ +- + id: 1 + repo_id: 1 diff --git a/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml new file mode 100644 index 0000000000..7f3255096d --- /dev/null +++ b/models/migrations/fixtures/Test_AddRepoIDForAttachment/release.yml @@ -0,0 +1,3 @@ +- + id: 1 + repo_id: 1 diff --git a/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml new file mode 100644 index 0000000000..6feaeb39f0 --- /dev/null +++ b/models/migrations/fixtures/Test_AddUniqueIndexForProjectIssue/project_issue.yml @@ -0,0 +1,9 @@ +- + id: 1 + project_id: 1 + issue_id: 1 + +- + id: 2 + project_id: 1 + issue_id: 1 diff --git a/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml new file mode 100644 index 0000000000..2450d20beb --- /dev/null +++ b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project.yml @@ -0,0 +1,23 @@ +- + id: 1 + title: project without default column + owner_id: 2 + repo_id: 0 + is_closed: false + creator_id: 2 + board_type: 1 + type: 2 + created_unix: 1688973000 + updated_unix: 1688973000 + +- + id: 2 + title: project with multiple default columns + owner_id: 2 + repo_id: 0 + is_closed: false + creator_id: 2 + board_type: 1 + type: 2 + created_unix: 1688973000 + updated_unix: 1688973000 diff --git a/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml new file mode 100644 index 0000000000..2e1b1c7eee --- /dev/null +++ b/models/migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml @@ -0,0 +1,26 @@ +- + id: 1 + project_id: 1 + title: Done + creator_id: 2 + default: false + created_unix: 1588117528 + updated_unix: 1588117528 + +- + id: 2 + project_id: 2 + title: Backlog + creator_id: 2 + default: true + created_unix: 1588117528 + updated_unix: 1588117528 + +- + id: 3 + project_id: 2 + title: Uncategorized + creator_id: 2 + default: true + created_unix: 1588117528 + updated_unix: 1588117528 diff --git a/models/migrations/fixtures/Test_RepositoryFormat/comment.yml b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml new file mode 100644 index 0000000000..1197b086e3 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/comment.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml new file mode 100644 index 0000000000..ca0aaec4cc --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/commit_status.yml @@ -0,0 +1,3 @@ +- + id: 1 + context_hash: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml new file mode 100644 index 0000000000..380cc079ee --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/pull_request.yml @@ -0,0 +1,5 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d + merge_base: 19fe5caf872476db265596eaac1dc35ad1c6422d + merged_commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/release.yml b/models/migrations/fixtures/Test_RepositoryFormat/release.yml new file mode 100644 index 0000000000..ffabe4ab9e --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/release.yml @@ -0,0 +1,3 @@ +- + id: 1 + sha1: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml new file mode 100644 index 0000000000..f04cb3b340 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_archiver.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_id: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml new file mode 100644 index 0000000000..1197b086e3 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/repo_indexer_status.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml new file mode 100644 index 0000000000..1197b086e3 --- /dev/null +++ b/models/migrations/fixtures/Test_RepositoryFormat/review_state.yml @@ -0,0 +1,3 @@ +- + id: 1 + commit_sha: 19fe5caf872476db265596eaac1dc35ad1c6422d diff --git a/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml new file mode 100644 index 0000000000..7025144106 --- /dev/null +++ b/models/migrations/fixtures/Test_UpdateBadgeColName/badge.yml @@ -0,0 +1,4 @@ +- + id: 1 + description: the badge + image_url: https://gitea.com/myimage.png diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index beb1f3bb96..3ea8f2acbf 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/models/migrations/v1_20" "code.gitea.io/gitea/models/migrations/v1_21" "code.gitea.io/gitea/models/migrations/v1_22" + "code.gitea.io/gitea/models/migrations/v1_23" "code.gitea.io/gitea/models/migrations/v1_6" "code.gitea.io/gitea/models/migrations/v1_7" "code.gitea.io/gitea/models/migrations/v1_8" @@ -558,6 +559,27 @@ var migrations = []Migration{ NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun), // v286 -> v287 NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256), + // v287 -> v288 + NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges), + // v288 -> v289 + NewMigration("Add user_blocking table", v1_22.AddUserBlockingTable), + // v289 -> v290 + NewMigration("Add default_wiki_branch to repository table", v1_22.AddDefaultWikiBranch), + // v290 -> v291 + NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), + // v291 -> v292 + NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment), + // v292 -> v293 + NewMigration("Ensure every project has exactly one default column - No Op", noopMigration), + // v293 -> v294 + NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency), + + // Gitea 1.22.0 ends at 294 + + // v294 -> v295 + NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue), + // v295 -> v296 + NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_14/v166.go b/models/migrations/v1_14/v166.go index 78f33e8f9b..e5731582fd 100644 --- a/models/migrations/v1_14/v166.go +++ b/models/migrations/v1_14/v166.go @@ -4,9 +4,9 @@ package v1_14 //nolint import ( + "crypto/sha256" "encoding/hex" - "github.com/minio/sha256-simd" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/pbkdf2" diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index 17669a012e..d99bbc2962 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -15,7 +15,6 @@ func Test_AddRepoIDForAttachment(t *testing.T) { type Attachment struct { ID int64 `xorm:"pk autoincr"` UUID string `xorm:"uuid UNIQUE"` - RepoID int64 `xorm:"INDEX"` // this should not be zero IssueID int64 `xorm:"INDEX"` // maybe zero when creating ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating UploaderID int64 `xorm:"INDEX DEFAULT 0"` @@ -44,12 +43,21 @@ func Test_AddRepoIDForAttachment(t *testing.T) { return } - var issueAttachments []*Attachment - err := x.Where("issue_id > 0").Find(&issueAttachments) + type NewAttachment struct { + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"uuid UNIQUE"` + RepoID int64 `xorm:"INDEX"` // this should not be zero + IssueID int64 `xorm:"INDEX"` // maybe zero when creating + ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating + UploaderID int64 `xorm:"INDEX DEFAULT 0"` + } + + var issueAttachments []*NewAttachment + err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments) assert.NoError(t, err) for _, attach := range issueAttachments { - assert.Greater(t, attach.RepoID, 0) - assert.Greater(t, attach.IssueID, 0) + assert.Greater(t, attach.RepoID, int64(0)) + assert.Greater(t, attach.IssueID, int64(0)) var issue Issue has, err := x.ID(attach.IssueID).Get(&issue) assert.NoError(t, err) @@ -57,12 +65,12 @@ func Test_AddRepoIDForAttachment(t *testing.T) { assert.EqualValues(t, attach.RepoID, issue.RepoID) } - var releaseAttachments []*Attachment - err = x.Where("release_id > 0").Find(&releaseAttachments) + var releaseAttachments []*NewAttachment + err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments) assert.NoError(t, err) for _, attach := range releaseAttachments { - assert.Greater(t, attach.RepoID, 0) - assert.Greater(t, attach.IssueID, 0) + assert.Greater(t, attach.RepoID, int64(0)) + assert.Greater(t, attach.ReleaseID, int64(0)) var release Release has, err := x.ID(attach.ReleaseID).Get(&release) assert.NoError(t, err) diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go index b2b94845d9..0a45c51245 100644 --- a/models/migrations/v1_22/v283.go +++ b/models/migrations/v1_22/v283.go @@ -4,7 +4,10 @@ package v1_22 //nolint import ( + "fmt" + "xorm.io/xorm" + "xorm.io/xorm/schemas" ) func AddCombinedIndexToIssueUser(x *xorm.Engine) error { @@ -20,8 +23,18 @@ func AddCombinedIndexToIssueUser(x *xorm.Engine) error { return err } for _, issueUser := range duplicatedIssueUsers { - if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil { - return err + if x.Dialect().URI().DBType == schemas.MSSQL { + if _, err := x.Exec(fmt.Sprintf("delete from issue_user where id in (SELECT top %d id FROM issue_user WHERE issue_id = ? and uid = ?)", issueUser.Cnt-1), issueUser.IssueID, issueUser.UID); err != nil { + return err + } + } else { + var ids []int64 + if err := x.SQL("SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1).Find(&ids); err != nil { + return err + } + if _, err := x.Table("issue_user").In("id", ids).Delete(); err != nil { + return err + } } } diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go index ef19f64221..fbbd87344f 100644 --- a/models/migrations/v1_22/v286.go +++ b/models/migrations/v1_22/v286.go @@ -36,9 +36,9 @@ func expandHashReferencesToSha256(x *xorm.Engine) error { if setting.Database.Type.IsMSSQL() { // drop indexes that need to be re-created afterwards droppedIndexes := []string{ - "DROP INDEX commit_status.IDX_commit_status_context_hash", - "DROP INDEX review_state.UQE_review_state_pull_commit_user", - "DROP INDEX repo_archiver.UQE_repo_archiver_s", + "DROP INDEX IF EXISTS [IDX_commit_status_context_hash] ON [commit_status]", + "DROP INDEX IF EXISTS [UQE_review_state_pull_commit_user] ON [review_state]", + "DROP INDEX IF EXISTS [UQE_repo_archiver_s] ON [repo_archiver]", } for _, s := range droppedIndexes { _, err := db.Exec(s) @@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error { if setting.Database.Type.IsMySQL() { _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) } else if setting.Database.Type.IsMSSQL() { - _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) + _, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1])) } else { _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1])) } diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index e36a18a116..7c353747e3 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -17,14 +17,72 @@ func PrepareOldRepository(t *testing.T) (*xorm.Engine, func()) { ID int64 `xorm:"pk autoincr"` } + type CommitStatus struct { + ID int64 + ContextHash string + } + + type RepoArchiver struct { + ID int64 + RepoID int64 + Type int + CommitID string + } + + type ReviewState struct { + ID int64 + CommitSHA string + UserID int64 + PullID int64 + } + + type Comment struct { + ID int64 + CommitSHA string + } + + type PullRequest struct { + ID int64 + CommitSHA string + MergeBase string + MergedCommitID string + } + + type Release struct { + ID int64 + Sha1 string + } + + type RepoIndexerStatus struct { + ID int64 + CommitSHA string + } + + type Review struct { + ID int64 + CommitID string + } + // Prepare and load the testing database - return base.PrepareTestEnv(t, 0, new(Repository)) + return base.PrepareTestEnv(t, 0, + new(Repository), + new(CommitStatus), + new(RepoArchiver), + new(ReviewState), + new(Review), + new(Comment), + new(PullRequest), + new(Release), + new(RepoIndexerStatus), + ) } func Test_RepositoryFormat(t *testing.T) { x, deferable := PrepareOldRepository(t) defer deferable() + assert.NoError(t, AdjustDBForSha256(x)) + type Repository struct { ID int64 `xorm:"pk autoincr"` ObjectFormatName string `xorg:"not null default('sha1')"` @@ -37,12 +95,10 @@ func Test_RepositoryFormat(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 4, count) - assert.NoError(t, AdjustDBForSha256(x)) - - repo.ID = 20 repo.ObjectFormatName = "sha256" _, err = x.Insert(repo) assert.NoError(t, err) + id := repo.ID count, err = x.Count(new(Repository)) assert.NoError(t, err) @@ -55,7 +111,7 @@ func Test_RepositoryFormat(t *testing.T) { assert.EqualValues(t, "sha1", repo.ObjectFormatName) repo = new(Repository) - ok, err = x.ID(20).Get(repo) + ok, err = x.ID(id).Get(repo) assert.NoError(t, err) assert.EqualValues(t, true, ok) assert.EqualValues(t, "sha256", repo.ObjectFormatName) diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go new file mode 100644 index 0000000000..c8b1593286 --- /dev/null +++ b/models/migrations/v1_22/v287.go @@ -0,0 +1,46 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +type BadgeUnique struct { + ID int64 `xorm:"pk autoincr"` + Slug string `xorm:"UNIQUE"` +} + +func (BadgeUnique) TableName() string { + return "badge" +} + +func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error { + type Badge struct { + Slug string + } + + err := x.Sync(new(Badge)) + if err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + _, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL") + if err != nil { + return err + } + + err = sess.Sync(new(BadgeUnique)) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go new file mode 100644 index 0000000000..9c7b10947d --- /dev/null +++ b/models/migrations/v1_22/v287_test.go @@ -0,0 +1,57 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "fmt" + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" +) + +func Test_UpdateBadgeColName(t *testing.T) { + type Badge struct { + ID int64 `xorm:"pk autoincr"` + Description string + ImageURL string + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(Badge)) + defer deferable() + if x == nil || t.Failed() { + return + } + + oldBadges := []*Badge{ + {Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"}, + {Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"}, + {Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"}, + } + + for _, badge := range oldBadges { + _, err := x.Insert(badge) + assert.NoError(t, err) + } + + if err := UseSlugInsteadOfIDForBadges(x); err != nil { + assert.NoError(t, err) + return + } + + got := []BadgeUnique{} + if err := x.Table("badge").Asc("id").Find(&got); !assert.NoError(t, err) { + return + } + + for i, e := range oldBadges { + got := got[i+1] // 1 is in the badge.yml + assert.Equal(t, e.ID, got.ID) + assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug) + } + + // TODO: check if badges have been updated +} diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go new file mode 100644 index 0000000000..7c93bfcc66 --- /dev/null +++ b/models/migrations/v1_22/v288.go @@ -0,0 +1,26 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +type Blocking struct { + ID int64 `xorm:"pk autoincr"` + BlockerID int64 `xorm:"UNIQUE(block)"` + BlockeeID int64 `xorm:"UNIQUE(block)"` + Note string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +func (*Blocking) TableName() string { + return "user_blocking" +} + +func AddUserBlockingTable(x *xorm.Engine) error { + return x.Sync(&Blocking{}) +} diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go new file mode 100644 index 0000000000..e2dfc48715 --- /dev/null +++ b/models/migrations/v1_22/v289.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import "xorm.io/xorm" + +func AddDefaultWikiBranch(x *xorm.Engine) error { + type Repository struct { + ID int64 + DefaultWikiBranch string + } + if err := x.Sync(&Repository{}); err != nil { + return err + } + _, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')") + return err +} diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go new file mode 100644 index 0000000000..e9b7f504ba --- /dev/null +++ b/models/migrations/v1_22/v290.go @@ -0,0 +1,17 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "xorm.io/xorm" +) + +type HookTask struct { + PayloadVersion int `xorm:"DEFAULT 1"` +} + +func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error { + // create missing column + return x.Sync(new(HookTask)) +} diff --git a/models/migrations/v1_22/v291.go b/models/migrations/v1_22/v291.go new file mode 100644 index 0000000000..0bfffe5d05 --- /dev/null +++ b/models/migrations/v1_22/v291.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import "xorm.io/xorm" + +func AddCommentIDIndexofAttachment(x *xorm.Engine) error { + type Attachment struct { + CommentID int64 `xorm:"INDEX"` + } + + return x.Sync(&Attachment{}) +} diff --git a/models/migrations/v1_22/v292.go b/models/migrations/v1_22/v292.go new file mode 100644 index 0000000000..beca556aee --- /dev/null +++ b/models/migrations/v1_22/v292.go @@ -0,0 +1,9 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +// NOTE: noop the original migration has bug which some projects will be skip, so +// these projects will have no default board. +// So that this migration will be skipped and go to v293.go +// This file is a placeholder so that readers can know what happened diff --git a/models/migrations/v1_22/v293.go b/models/migrations/v1_22/v293.go new file mode 100644 index 0000000000..53cc719294 --- /dev/null +++ b/models/migrations/v1_22/v293.go @@ -0,0 +1,108 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +// CheckProjectColumnsConsistency ensures there is exactly one default board per project present +func CheckProjectColumnsConsistency(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + + limit := setting.Database.IterateBufferSize + if limit <= 0 { + limit = 50 + } + + type Project struct { + ID int64 + CreatorID int64 + BoardID int64 + } + + type ProjectBoard struct { + ID int64 `xorm:"pk autoincr"` + Title string + Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board + Sorting int8 `xorm:"NOT NULL DEFAULT 0"` + Color string `xorm:"VARCHAR(7)"` + + ProjectID int64 `xorm:"INDEX NOT NULL"` + CreatorID int64 `xorm:"NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + for { + if err := sess.Begin(); err != nil { + return err + } + + // all these projects without defaults will be fixed in the same loop, so + // we just need to always get projects without defaults until no such project + var projects []*Project + if err := sess.Select("project.id as id, project.creator_id, project_board.id as board_id"). + Join("LEFT", "project_board", "project_board.project_id = project.id AND project_board.`default`=?", true). + Where("project_board.id is NULL OR project_board.id = 0"). + Limit(limit). + Find(&projects); err != nil { + return err + } + + for _, p := range projects { + if _, err := sess.Insert(ProjectBoard{ + ProjectID: p.ID, + Default: true, + Title: "Uncategorized", + CreatorID: p.CreatorID, + }); err != nil { + return err + } + } + if err := sess.Commit(); err != nil { + return err + } + + if len(projects) == 0 { + break + } + } + sess.Close() + + return removeDuplicatedBoardDefault(x) +} + +func removeDuplicatedBoardDefault(x *xorm.Engine) error { + type ProjectInfo struct { + ProjectID int64 + DefaultNum int + } + var projects []ProjectInfo + if err := x.Select("project_id, count(*) AS default_num"). + Table("project_board"). + Where("`default` = ?", true). + GroupBy("project_id"). + Having("count(*) > 1"). + Find(&projects); err != nil { + return err + } + + for _, project := range projects { + if _, err := x.Where("project_id=?", project.ProjectID). + Table("project_board"). + Limit(project.DefaultNum - 1). + Update(map[string]bool{ + "`default`": false, + }); err != nil { + return err + } + } + return nil +} diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go new file mode 100644 index 0000000000..ccc92f39a6 --- /dev/null +++ b/models/migrations/v1_22/v293_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/models/project" + + "github.com/stretchr/testify/assert" +) + +func Test_CheckProjectColumnsConsistency(t *testing.T) { + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board)) + defer deferable() + if x == nil || t.Failed() { + return + } + + assert.NoError(t, CheckProjectColumnsConsistency(x)) + + // check if default board was added + var defaultBoard project.Board + has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard) + assert.NoError(t, err) + assert.True(t, has) + assert.Equal(t, int64(1), defaultBoard.ProjectID) + assert.True(t, defaultBoard.Default) + + // check if multiple defaults, previous were removed and last will be kept + expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2) + assert.NoError(t, err) + assert.Equal(t, int64(2), expectDefaultBoard.ProjectID) + assert.False(t, expectDefaultBoard.Default) + + expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3) + assert.NoError(t, err) + assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID) + assert.True(t, expectNonDefaultBoard.Default) +} diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go new file mode 100644 index 0000000000..b7948bd4dd --- /dev/null +++ b/models/migrations/v1_23/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" +) + +func TestMain(m *testing.M) { + base.MainTest(m) +} diff --git a/models/migrations/v1_23/v294.go b/models/migrations/v1_23/v294.go new file mode 100644 index 0000000000..f2a54f6d23 --- /dev/null +++ b/models/migrations/v1_23/v294.go @@ -0,0 +1,53 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "fmt" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +// AddUniqueIndexForProjectIssue adds unique indexes for project issue table +func AddUniqueIndexForProjectIssue(x *xorm.Engine) error { + // remove possible duplicated records in table project_issue + type result struct { + IssueID int64 + ProjectID int64 + Cnt int + } + var results []result + if err := x.Select("issue_id, project_id, count(*) as cnt"). + Table("project_issue"). + GroupBy("issue_id, project_id"). + Having("count(*) > 1"). + Find(&results); err != nil { + return err + } + for _, r := range results { + if x.Dialect().URI().DBType == schemas.MSSQL { + if _, err := x.Exec(fmt.Sprintf("delete from project_issue where id in (SELECT top %d id FROM project_issue WHERE issue_id = ? and project_id = ?)", r.Cnt-1), r.IssueID, r.ProjectID); err != nil { + return err + } + } else { + var ids []int64 + if err := x.SQL("SELECT id FROM project_issue WHERE issue_id = ? and project_id = ? limit ?", r.IssueID, r.ProjectID, r.Cnt-1).Find(&ids); err != nil { + return err + } + if _, err := x.Table("project_issue").In("id", ids).Delete(); err != nil { + return err + } + } + } + + // add unique index for project_issue table + type ProjectIssue struct { //revive:disable-line:exported + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX unique(s)"` + ProjectID int64 `xorm:"INDEX unique(s)"` + } + + return x.Sync(new(ProjectIssue)) +} diff --git a/models/migrations/v1_23/v294_test.go b/models/migrations/v1_23/v294_test.go new file mode 100644 index 0000000000..d9a44ad866 --- /dev/null +++ b/models/migrations/v1_23/v294_test.go @@ -0,0 +1,52 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "slices" + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" + "xorm.io/xorm/schemas" +) + +func Test_AddUniqueIndexForProjectIssue(t *testing.T) { + type ProjectIssue struct { //revive:disable-line:exported + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX"` + ProjectID int64 `xorm:"INDEX"` + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue)) + defer deferable() + if x == nil || t.Failed() { + return + } + + cnt, err := x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count() + assert.NoError(t, err) + assert.EqualValues(t, 2, cnt) + + assert.NoError(t, AddUniqueIndexForProjectIssue(x)) + + cnt, err = x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count() + assert.NoError(t, err) + assert.EqualValues(t, 1, cnt) + + tables, err := x.DBMetas() + assert.NoError(t, err) + assert.EqualValues(t, 1, len(tables)) + found := false + for _, index := range tables[0].Indexes { + if index.Type == schemas.UniqueType { + found = true + slices.Equal(index.Cols, []string{"project_id", "issue_id"}) + break + } + } + assert.True(t, found) +} diff --git a/models/migrations/v1_23/v295.go b/models/migrations/v1_23/v295.go new file mode 100644 index 0000000000..9a2003cfc1 --- /dev/null +++ b/models/migrations/v1_23/v295.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import "xorm.io/xorm" + +func AddCommitStatusSummary(x *xorm.Engine) error { + type CommitStatusSummary struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"` + State string `xorm:"VARCHAR(7) NOT NULL"` + } + // there is no migrations because if there is no data on this table, it will fall back to get data + // from commit status + return x.Sync2(new(CommitStatusSummary)) +} diff --git a/models/org.go b/models/org.go index 5f61f05b16..69cc47137e 100644 --- a/models/org.go +++ b/models/org.go @@ -12,15 +12,16 @@ import ( "code.gitea.io/gitea/models/organization" 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" ) // RemoveOrgUser removes user from given organization. -func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { +func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error { ou := new(organization.OrgUser) has, err := db.GetEngine(ctx). - Where("uid=?", userID). - And("org_id=?", orgID). + Where("uid=?", user.ID). + And("org_id=?", org.ID). Get(ou) if err != nil { return fmt.Errorf("get org-user: %w", err) @@ -28,13 +29,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { return nil } - org, err := organization.GetOrgByID(ctx, orgID) - if err != nil { - return fmt.Errorf("GetUserByID [%d]: %w", orgID, err) - } - // Check if the user to delete is the last member in owner team. - if isOwner, err := organization.IsOrganizationOwner(ctx, orgID, userID); err != nil { + if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil { return err } else if isOwner { t, err := organization.GetOwnerTeam(ctx, org.ID) @@ -45,8 +41,8 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { if err := t.LoadMembers(ctx); err != nil { return err } - if t.Members[0].ID == userID { - return organization.ErrLastOrgOwner{UID: userID} + if t.Members[0].ID == user.ID { + return organization.ErrLastOrgOwner{UID: user.ID} } } } @@ -59,28 +55,32 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil { return err - } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil { + } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil { return err } // Delete all repository accesses and unwatch them. - env, err := organization.AccessibleReposEnv(ctx, org, userID) + env, err := organization.AccessibleReposEnv(ctx, org, user.ID) if err != nil { return fmt.Errorf("AccessibleReposEnv: %w", err) } repoIDs, err := env.RepoIDs(1, org.NumRepos) if err != nil { - return fmt.Errorf("GetUserRepositories [%d]: %w", userID, err) + return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err) } for _, repoID := range repoIDs { - if err = repo_model.WatchRepo(ctx, userID, repoID, false); err != nil { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return err + } + if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil { return err } } if len(repoIDs) > 0 { if _, err = db.GetEngine(ctx). - Where("user_id = ?", userID). + Where("user_id = ?", user.ID). In("repo_id", repoIDs). Delete(new(access_model.Access)); err != nil { return err @@ -88,12 +88,12 @@ func RemoveOrgUser(ctx context.Context, orgID, userID int64) error { } // Delete member in their teams. - teams, err := organization.GetUserOrgTeams(ctx, org.ID, userID) + teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID) if err != nil { return err } for _, t := range teams { - if err = removeTeamMember(ctx, t, userID); err != nil { + if err = removeTeamMember(ctx, t, user); err != nil { return err } } diff --git a/models/org_team.go b/models/org_team.go index 1a452436c3..aecf0d80fd 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -44,7 +44,7 @@ func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.R return fmt.Errorf("getMembers: %w", err) } for _, u := range t.Members { - if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil { + if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil { return fmt.Errorf("watchRepo: %w", err) } } @@ -125,7 +125,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error continue } - if err = repo_model.WatchRepo(ctx, user.ID, repo.ID, false); err != nil { + if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil { return err } @@ -341,7 +341,7 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error { } for _, tm := range t.Members { - if err := removeInvalidOrgUser(ctx, tm.ID, t.OrgID); err != nil { + if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil { return err } } @@ -356,19 +356,23 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error { // AddTeamMember adds new membership of given team to given organization, // the user will have membership to given organization automatically when needed. -func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error { - isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) +func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { + if user_model.IsUserBlockedBy(ctx, user, team.OrgID) { + return user_model.ErrBlockedUser + } + + isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) if err != nil || isAlreadyMember { return err } - if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil { + if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil { return err } err = db.WithTx(ctx, func(ctx context.Context) error { // check in transaction - isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) + isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) if err != nil || isAlreadyMember { return err } @@ -376,7 +380,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e sess := db.GetEngine(ctx) if err := db.Insert(ctx, &organization.TeamUser{ - UID: userID, + UID: user.ID, OrgID: team.OrgID, TeamID: team.ID, }); err != nil { @@ -392,7 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e subQuery := builder.Select("repo_id").From("team_repo"). Where(builder.Eq{"team_id": team.ID}) - if _, err := sess.Where("user_id=?", userID). + if _, err := sess.Where("user_id=?", user.ID). In("repo_id", subQuery). And("mode < ?", team.AccessMode). SetExpr("mode", team.AccessMode). @@ -402,14 +406,14 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e // for not exist access var repoIDs []int64 - accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": userID}) + accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID}) if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil { return fmt.Errorf("select id accesses: %w", err) } accesses := make([]*access_model.Access, 0, 100) for i, repoID := range repoIDs { - accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: userID, Mode: team.AccessMode}) + accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode}) if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 { if err = db.Insert(ctx, accesses); err != nil { return fmt.Errorf("insert new user accesses: %w", err) @@ -430,10 +434,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e if err := team.LoadRepositories(ctx); err != nil { log.Error("team.LoadRepositories failed: %v", err) } + // FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment go func(repos []*repo_model.Repository) { for _, repo := range repos { - if err = repo_model.WatchRepo(db.DefaultContext, userID, repo.ID, true); err != nil { + if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil { log.Error("watch repo failed: %v", err) } } @@ -443,16 +448,16 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e return nil } -func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error { +func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { e := db.GetEngine(ctx) - isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) + isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID) if err != nil || !isMember { return err } // Check if the user to delete is the last member in owner team. if team.IsOwnerTeam() && team.NumMembers == 1 { - return organization.ErrLastOrgOwner{UID: userID} + return organization.ErrLastOrgOwner{UID: user.ID} } team.NumMembers-- @@ -462,7 +467,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 } if _, err := e.Delete(&organization.TeamUser{ - UID: userID, + UID: user.ID, OrgID: team.OrgID, TeamID: team.ID, }); err != nil { @@ -476,76 +481,76 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 // Delete access to team repositories. for _, repo := range team.Repos { - if err := access_model.RecalculateUserAccess(ctx, repo, userID); err != nil { + if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil { return err } // Remove watches from now unaccessible - if err := ReconsiderWatches(ctx, repo, userID); err != nil { + if err := ReconsiderWatches(ctx, repo, user); err != nil { return err } // Remove issue assignments from now unaccessible - if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { + if err := ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil { return err } } - return removeInvalidOrgUser(ctx, userID, team.OrgID) + return removeInvalidOrgUser(ctx, team.OrgID, user) } -func removeInvalidOrgUser(ctx context.Context, userID, orgID int64) error { +func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error { // Check if the user is a member of any team in the organization. if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{ - UID: userID, + UID: user.ID, OrgID: orgID, }); err != nil { return err } else if count == 0 { - return RemoveOrgUser(ctx, orgID, userID) + org, err := organization.GetOrgByID(ctx, orgID) + if err != nil { + return err + } + + return RemoveOrgUser(ctx, org, user) } return nil } // RemoveTeamMember removes member from given team of given organization. -func RemoveTeamMember(ctx context.Context, team *organization.Team, userID int64) error { +func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - if err := removeTeamMember(ctx, team, userID); err != nil { + if err := removeTeamMember(ctx, team, user); err != nil { return err } return committer.Commit() } -func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { - user, err := user_model.GetUserByID(ctx, uid) - if err != nil { - return err - } - +func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error { if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { return err } - if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). + if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}). In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). Delete(&issues_model.IssueAssignees{}); err != nil { - return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) + return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err) } return nil } -func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { - if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { +func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error { + if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has { return err } - if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { + if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil { return err } // Remove all IssueWatches a user has subscribed to in the repository - return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) + return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID) } diff --git a/models/org_team_test.go b/models/org_team_test.go index e4b7b917e8..cf2c8be536 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -21,33 +21,42 @@ import ( func TestTeam_AddMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - test := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID)) - unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID}) + test := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, AddTeamMember(db.DefaultContext, team, user)) + unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID}) } - test(1, 2) - test(1, 4) - test(3, 2) + + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + test(team1, user2) + test(team1, user4) + test(team3, user2) } func TestTeam_RemoveMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID)) - unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}) + testSuccess := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user)) + unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}) } - testSuccess(1, 4) - testSuccess(2, 2) - testSuccess(3, 2) - testSuccess(3, unittest.NonexistentID) - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) - err := RemoveTeamMember(db.DefaultContext, team, 2) + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + testSuccess(team1, user4) + testSuccess(team2, user2) + testSuccess(team3, user2) + + err := RemoveTeamMember(db.DefaultContext, team1, user2) assert.True(t, organization.IsErrLastOrgOwner(err)) } @@ -120,33 +129,42 @@ func TestDeleteTeam(t *testing.T) { func TestAddTeamMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - test := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, AddTeamMember(db.DefaultContext, team, userID)) - unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID}) + test := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, AddTeamMember(db.DefaultContext, team, user)) + unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID}) } - test(1, 2) - test(1, 4) - test(3, 2) + + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + test(team1, user2) + test(team1, user4) + test(team3, user2) } func TestRemoveTeamMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, userID)) - unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}) + testSuccess := func(team *organization.Team, user *user_model.User) { + assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user)) + unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}) } - testSuccess(1, 4) - testSuccess(2, 2) - testSuccess(3, 2) - testSuccess(3, unittest.NonexistentID) - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) - err := RemoveTeamMember(db.DefaultContext, team, 2) + team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1}) + team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + testSuccess(team1, user4) + testSuccess(team2, user2) + testSuccess(team3, user2) + + err := RemoveTeamMember(db.DefaultContext, team1, user2) assert.True(t, organization.IsErrLastOrgOwner(err)) } @@ -155,15 +173,15 @@ func TestRepository_RecalculateAccesses3(t *testing.T) { team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5}) user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) - has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23}) + has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23}) assert.NoError(t, err) assert.False(t, has) // adding user29 to team5 should add an explicit access row for repo 23 // even though repo 23 is public - assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29.ID)) + assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29)) - has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: 29, RepoID: 23}) + has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23}) assert.NoError(t, err) assert.True(t, has) } diff --git a/models/org_test.go b/models/org_test.go index d10a1dc218..247530406d 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -16,22 +16,27 @@ import ( func TestUser_RemoveMember(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // remove a user that is a member - unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: 4, OrgID: 3}) + unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID}) prevNumMembers := org.NumMembers - assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 4)) - unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 4, OrgID: 3}) - org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4)) + unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID}) + + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID}) assert.Equal(t, prevNumMembers-1, org.NumMembers) // remove a user that is not a member - unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3}) + unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID}) prevNumMembers = org.NumMembers - assert.NoError(t, RemoveOrgUser(db.DefaultContext, org.ID, 5)) - unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: 5, OrgID: 3}) - org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5)) + unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID}) + + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID}) assert.Equal(t, prevNumMembers, org.NumMembers) unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) @@ -39,23 +44,31 @@ func TestUser_RemoveMember(t *testing.T) { func TestRemoveOrgUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(orgID, userID int64) { - org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) + + testSuccess := func(org *organization.Organization, user *user_model.User) { expectedNumMembers := org.NumMembers - if unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) { + if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) { expectedNumMembers-- } - assert.NoError(t, RemoveOrgUser(db.DefaultContext, orgID, userID)) - unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) - org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) + assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user)) + unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) + org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID}) assert.EqualValues(t, expectedNumMembers, org.NumMembers) } - testSuccess(3, 4) - testSuccess(3, 4) - err := RemoveOrgUser(db.DefaultContext, 7, 5) + org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + testSuccess(org3, user4) + + org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + testSuccess(org3, user4) + + err := RemoveOrgUser(db.DefaultContext, org7, user5) assert.Error(t, err) assert.True(t, organization.IsErrLastOrgOwner(err)) - unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: 7, UID: 5}) + unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID}) unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) } diff --git a/models/organization/org.go b/models/organization/org.go index 23a4e2f96a..b33d15d29c 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" @@ -319,8 +320,9 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode // Add initial creator to organization and owner team. if err = db.Insert(ctx, &OrgUser{ - UID: owner.ID, - OrgID: org.ID, + UID: owner.ID, + OrgID: org.ID, + IsPublic: setting.Service.DefaultOrgMemberVisible, }); err != nil { return fmt.Errorf("insert org-user relation: %w", err) } @@ -400,6 +402,9 @@ func DeleteOrganization(ctx context.Context, org *Organization) error { &TeamUnit{OrgID: org.ID}, &TeamInvite{OrgID: org.ID}, &secret_model.Secret{OwnerID: org.ID}, + &user_model.Blocking{BlockerID: org.ID}, + &actions_model.ActionRunner{OwnerID: org.ID}, + &actions_model.ActionRunnerToken{OwnerID: org.ID}, ); err != nil { return fmt.Errorf("DeleteBeans: %w", err) } @@ -594,9 +599,7 @@ func GetOrgByID(ctx context.Context, id int64) (*Organization, error) { return nil, err } else if !has { return nil, user_model.ErrUserNotExist{ - UID: id, - Name: "", - KeyID: 0, + UID: id, } } return u, nil diff --git a/models/organization/team_user.go b/models/organization/team_user.go index ab767db200..d6d0a5054d 100644 --- a/models/organization/team_user.go +++ b/models/organization/team_user.go @@ -30,14 +30,6 @@ func IsTeamMember(ctx context.Context, orgID, teamID, userID int64) (bool, error Exist() } -// GetTeamUsersByTeamID returns team users for a team -func GetTeamUsersByTeamID(ctx context.Context, teamID int64) ([]*TeamUser, error) { - teamUsers := make([]*TeamUser, 0, 10) - return teamUsers, db.GetEngine(ctx). - Where("team_id=?", teamID). - Find(&teamUsers) -} - // SearchMembersOptions holds the search options type SearchMembersOptions struct { db.ListOptions diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index f849ab5c04..b8ef698d38 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -70,16 +70,26 @@ type PackageFileDescriptor struct { Properties PackagePropertyList } -// PackageWebLink returns the package web link +// PackageWebLink returns the relative package web link func (pd *PackageDescriptor) PackageWebLink() string { return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) } -// FullWebLink returns the package version web link -func (pd *PackageDescriptor) FullWebLink() string { +// VersionWebLink returns the relative package version web link +func (pd *PackageDescriptor) VersionWebLink() string { return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion)) } +// PackageHTMLURL returns the absolute package HTML URL +func (pd *PackageDescriptor) PackageHTMLURL() string { + return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +} + +// VersionHTMLURL returns the absolute package version HTML URL +func (pd *PackageDescriptor) VersionHTMLURL() string { + return fmt.Sprintf("%s/%s", pd.PackageHTMLURL(), url.PathEscape(pd.Version.LowerVersion)) +} + // CalculateBlobSize returns the total blobs size in bytes func (pd *PackageDescriptor) CalculateBlobSize() int64 { size := int64(0) diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go index 53cdf2d4ad..7a505ff08f 100644 --- a/models/packages/nuget/search.go +++ b/models/packages/nuget/search.go @@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption func toConds(opts *packages_model.PackageSearchOptions) builder.Cond { var cond builder.Cond = builder.Eq{ - "package.is_internal": opts.IsInternal.IsTrue(), + "package.is_internal": opts.IsInternal.Value(), "package.owner_id": opts.OwnerID, "package.type": packages_model.TypeNuGet, } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 8fc475691b..505dbaa0a5 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -105,7 +106,7 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType ExactMatch: true, Value: version, }, - IsInternal: util.OptionalBoolOf(isInternal), + IsInternal: optional.Some(isInternal), Paginator: db.NewAbsoluteListOptions(0, 1), }) if err != nil { @@ -122,7 +123,7 @@ func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Ty pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ OwnerID: ownerID, Type: packageType, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) return pvs, err } @@ -136,7 +137,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty ExactMatch: true, Value: name, }, - IsInternal: util.OptionalBoolFalse, + IsInternal: optional.Some(false), }) return pvs, err } @@ -182,18 +183,18 @@ type PackageSearchOptions struct { Name SearchValue // only results with the specific name are found Version SearchValue // only results with the specific version are found Properties map[string]string // only results are found which contain all listed version properties with the specific value - IsInternal util.OptionalBool - HasFileWithName string // only results are found which are associated with a file with the specific name - HasFiles util.OptionalBool // only results are found which have associated files + IsInternal optional.Option[bool] + HasFileWithName string // only results are found which are associated with a file with the specific name + HasFiles optional.Option[bool] // only results are found which have associated files Sort VersionSort db.Paginator } func (opts *PackageSearchOptions) ToConds() builder.Cond { cond := builder.NewCond() - if !opts.IsInternal.IsNone() { + if opts.IsInternal.Has() { cond = builder.Eq{ - "package_version.is_internal": opts.IsInternal.IsTrue(), + "package_version.is_internal": opts.IsInternal.Value(), } } @@ -250,10 +251,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond { cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond))) } - if !opts.HasFiles.IsNone() { + if opts.HasFiles.Has() { filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id"))) - if opts.HasFiles.IsFalse() { + if !opts.HasFiles.Value() { filesCond = builder.Not{filesCond} } @@ -307,8 +308,8 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P And(builder.Expr("pv2.id IS NULL")) joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))") - if !opts.IsInternal.IsNone() { - joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.IsTrue()}) + if opts.IsInternal.Has() { + joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()}) } sess := db.GetEngine(ctx). diff --git a/models/perm/access/access.go b/models/perm/access/access.go index 3e2568b4b4..b422a08614 100644 --- a/models/perm/access/access.go +++ b/models/perm/access/access.go @@ -128,9 +128,9 @@ func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap // refreshCollaboratorAccesses retrieves repository collaborations with their access modes. func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error { - collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{}) + collaborators, _, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{RepoID: repoID}) if err != nil { - return fmt.Errorf("getCollaborations: %w", err) + return fmt.Errorf("GetCollaborators: %w", err) } for _, c := range collaborators { if c.User.IsGhost() { diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 395ecdf1a5..4175cb9b92 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -332,7 +332,6 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model. // CanBeAssigned return true if user can be assigned to issue or pull requests in repo // Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface. -// FIXME: user could send PullRequest also could be assigned??? func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) { if user.IsOrganization() { return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) @@ -341,7 +340,8 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model. if err != nil { return false, err } - return perm.CanAccessAny(perm_model.AccessModeWrite, unit.TypeCode, unit.TypeIssues, unit.TypePullRequests), nil + return perm.CanAccessAny(perm_model.AccessModeWrite, unit.AllRepoUnitTypes...) || + perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil } // HasAccess returns true if user has access to repo diff --git a/models/project/board.go b/models/project/board.go index 3e2d8e0472..5f142a356c 100644 --- a/models/project/board.go +++ b/models/project/board.go @@ -123,6 +123,17 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error { return nil } + board := Board{ + CreatedUnix: timeutil.TimeStampNow(), + CreatorID: project.CreatorID, + Title: "Backlog", + ProjectID: project.ID, + Default: true, + } + if err := db.Insert(ctx, board); err != nil { + return err + } + if len(items) == 0 { return nil } @@ -176,6 +187,10 @@ func deleteBoardByID(ctx context.Context, boardID int64) error { return err } + if board.Default { + return fmt.Errorf("deleteBoardByID: cannot delete default board") + } + if err = board.removeIssues(ctx); err != nil { return err } @@ -194,7 +209,6 @@ func deleteBoardByProjectID(ctx context.Context, projectID int64) error { // GetBoard fetches the current board of a project func GetBoard(ctx context.Context, boardID int64) (*Board, error) { board := new(Board) - has, err := db.GetEngine(ctx).ID(boardID).Get(board) if err != nil { return nil, err @@ -228,11 +242,10 @@ func UpdateBoard(ctx context.Context, board *Board) error { } // GetBoards fetches all boards related to a project -// if no default board set, first board is a temporary "Uncategorized" board func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { boards := make([]*Board, 0, 5) - if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("Sorting").Find(&boards); err != nil { + if err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, false).OrderBy("sorting").Find(&boards); err != nil { return nil, err } @@ -244,53 +257,64 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { return append([]*Board{defaultB}, boards...), nil } -// getDefaultBoard return default board and create a dummy if none exist +// getDefaultBoard return default board and ensure only one exists func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) { var board Board - exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board) + has, err := db.GetEngine(ctx). + Where("project_id=? AND `default` = ?", p.ID, true). + Desc("id").Get(&board) if err != nil { return nil, err } - if exist { + + if has { return &board, nil } - // represents a board for issues not assigned to one - return &Board{ + // create a default board if none is found + board = Board{ ProjectID: p.ID, - Title: "Uncategorized", Default: true, - }, nil + Title: "Uncategorized", + CreatorID: p.CreatorID, + } + if _, err := db.GetEngine(ctx).Insert(&board); err != nil { + return nil, err + } + return &board, nil } // SetDefaultBoard represents a board for issues not assigned to one -// if boardID is 0 unset default func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error { - _, err := db.GetEngine(ctx).Where(builder.Eq{ - "project_id": projectID, - "`default`": true, - }).Cols("`default`").Update(&Board{Default: false}) - if err != nil { - return err - } + return db.WithTx(ctx, func(ctx context.Context) error { + if _, err := GetBoard(ctx, boardID); err != nil { + return err + } - if boardID > 0 { - _, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}). + if _, err := db.GetEngine(ctx).Where(builder.Eq{ + "project_id": projectID, + "`default`": true, + }).Cols("`default`").Update(&Board{Default: false}); err != nil { + return err + } + + _, err := db.GetEngine(ctx).ID(boardID). + Where(builder.Eq{"project_id": projectID}). Cols("`default`").Update(&Board{Default: true}) - } - - return err + return err + }) } // UpdateBoardSorting update project board sorting func UpdateBoardSorting(ctx context.Context, bs BoardList) error { - for i := range bs { - _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols( - "sorting", - ).Update(bs[i]) - if err != nil { - return err + return db.WithTx(ctx, func(ctx context.Context) error { + for i := range bs { + if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols( + "sorting", + ).Update(bs[i]); err != nil { + return err + } } - } - return nil + return nil + }) } diff --git a/models/project/board_test.go b/models/project/board_test.go new file mode 100644 index 0000000000..71ba29a589 --- /dev/null +++ b/models/project/board_test.go @@ -0,0 +1,44 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestGetDefaultBoard(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5) + assert.NoError(t, err) + + // check if default board was added + board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext) + assert.NoError(t, err) + assert.Equal(t, int64(5), board.ProjectID) + assert.Equal(t, "Uncategorized", board.Title) + + projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6) + assert.NoError(t, err) + + // check if multiple defaults were removed + board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext) + assert.NoError(t, err) + assert.Equal(t, int64(6), board.ProjectID) + assert.Equal(t, int64(9), board.ID) + + // set 8 as default board + assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8)) + + // then 9 will become a non-default board + board, err = GetBoard(db.DefaultContext, 9) + assert.NoError(t, err) + assert.Equal(t, int64(6), board.ProjectID) + assert.False(t, board.Default) +} diff --git a/models/project/project.go b/models/project/project.go index d2fca6cdc8..8f9ee2a99e 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -6,11 +6,13 @@ package project import ( "context" "fmt" + "html/template" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" 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/timeutil" "code.gitea.io/gitea/modules/util" @@ -100,7 +102,7 @@ type Project struct { CardType CardType Type Type - RenderedContent string `xorm:"-"` + RenderedContent template.HTML `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -195,7 +197,7 @@ type SearchOptions struct { db.ListOptions OwnerID int64 RepoID int64 - IsClosed util.OptionalBool + IsClosed optional.Option[bool] OrderBy db.SearchOrderBy Type Type Title string @@ -206,11 +208,8 @@ func (opts SearchOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - switch opts.IsClosed { - case util.OptionalBoolTrue: - cond = cond.And(builder.Eq{"is_closed": true}) - case util.OptionalBoolFalse: - cond = cond.And(builder.Eq{"is_closed": false}) + if opts.IsClosed.Has() { + cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()}) } if opts.Type > 0 { diff --git a/models/project/project_test.go b/models/project/project_test.go index 7a37c1faf2..8fbbdedecf 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -92,19 +92,19 @@ func TestProjectsSort(t *testing.T) { }{ { sortType: "default", - wants: []int64{1, 3, 2, 4}, + wants: []int64{1, 3, 2, 6, 5, 4}, }, { sortType: "oldest", - wants: []int64{4, 2, 3, 1}, + wants: []int64{4, 5, 6, 2, 3, 1}, }, { sortType: "recentupdate", - wants: []int64{1, 3, 2, 4}, + wants: []int64{1, 3, 2, 6, 5, 4}, }, { sortType: "leastupdate", - wants: []int64{4, 2, 3, 1}, + wants: []int64{4, 5, 6, 2, 3, 1}, }, } @@ -113,8 +113,8 @@ func TestProjectsSort(t *testing.T) { OrderBy: GetSearchOrderByBySortType(tt.sortType), }) assert.NoError(t, err) - assert.EqualValues(t, int64(4), count) - if assert.Len(t, projects, 4) { + assert.EqualValues(t, int64(6), count) + if assert.Len(t, projects, 6) { for i := range projects { assert.EqualValues(t, tt.wants[i], projects[i].ID) } diff --git a/models/repo/attachment.go b/models/repo/attachment.go index 1a588398c1..9b0de11fdc 100644 --- a/models/repo/attachment.go +++ b/models/repo/attachment.go @@ -24,7 +24,7 @@ type Attachment struct { IssueID int64 `xorm:"INDEX"` // maybe zero when creating ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added - CommentID int64 + CommentID int64 `xorm:"INDEX"` Name string DownloadCount int64 `xorm:"DEFAULT 0"` Size int64 `xorm:"DEFAULT 0"` diff --git a/models/repo/collaboration.go b/models/repo/collaboration.go index 7288082614..272c6ac05b 100644 --- a/models/repo/collaboration.go +++ b/models/repo/collaboration.go @@ -36,14 +36,44 @@ type Collaborator struct { Collaboration *Collaboration } +type FindCollaborationOptions struct { + db.ListOptions + RepoID int64 + RepoOwnerID int64 + CollaboratorID int64 +} + +func (opts *FindCollaborationOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"collaboration.repo_id": opts.RepoID}) + } + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.Eq{"repository.owner_id": opts.RepoOwnerID}) + } + if opts.CollaboratorID != 0 { + cond = cond.And(builder.Eq{"collaboration.user_id": opts.CollaboratorID}) + } + return cond +} + +func (opts *FindCollaborationOptions) ToJoins() []db.JoinFunc { + if opts.RepoOwnerID != 0 { + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "repository", "repository.id = collaboration.repo_id") + return nil + }, + } + } + return nil +} + // GetCollaborators returns the collaborators for a repository -func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOptions) ([]*Collaborator, error) { - collaborations, err := db.Find[Collaboration](ctx, FindCollaborationOptions{ - ListOptions: listOptions, - RepoID: repoID, - }) +func GetCollaborators(ctx context.Context, opts *FindCollaborationOptions) ([]*Collaborator, int64, error) { + collaborations, total, err := db.FindAndCount[Collaboration](ctx, opts) if err != nil { - return nil, fmt.Errorf("db.Find[Collaboration]: %w", err) + return nil, 0, fmt.Errorf("db.FindAndCount[Collaboration]: %w", err) } collaborators := make([]*Collaborator, 0, len(collaborations)) @@ -54,7 +84,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti usersMap := make(map[int64]*user_model.User) if err := db.GetEngine(ctx).In("id", userIDs).Find(&usersMap); err != nil { - return nil, fmt.Errorf("Find users map by user ids: %w", err) + return nil, 0, fmt.Errorf("Find users map by user ids: %w", err) } for _, c := range collaborations { @@ -67,7 +97,7 @@ func GetCollaborators(ctx context.Context, repoID int64, listOptions db.ListOpti Collaboration: c, }) } - return collaborators, nil + return collaborators, total, nil } // GetCollaboration get collaboration for a repository id with a user id @@ -88,15 +118,6 @@ func IsCollaborator(ctx context.Context, repoID, userID int64) (bool, error) { return db.GetEngine(ctx).Get(&Collaboration{RepoID: repoID, UserID: userID}) } -type FindCollaborationOptions struct { - db.ListOptions - RepoID int64 -} - -func (opts FindCollaborationOptions) ToConds() builder.Cond { - return builder.And(builder.Eq{"repo_id": opts.RepoID}) -} - // ChangeCollaborationAccessMode sets new access mode for the collaboration. func ChangeCollaborationAccessMode(ctx context.Context, repo *Repository, uid int64, mode perm.AccessMode) error { // Discard invalid input diff --git a/models/repo/collaboration_test.go b/models/repo/collaboration_test.go index 21a99dd557..639050f5fd 100644 --- a/models/repo/collaboration_test.go +++ b/models/repo/collaboration_test.go @@ -19,7 +19,7 @@ func TestRepository_GetCollaborators(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) test := func(repoID int64) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) - collaborators, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{}) + collaborators, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{RepoID: repo.ID}) assert.NoError(t, err) expectedLen, err := db.GetEngine(db.DefaultContext).Count(&repo_model.Collaboration{RepoID: repoID}) assert.NoError(t, err) @@ -37,11 +37,17 @@ func TestRepository_GetCollaborators(t *testing.T) { // Test db.ListOptions repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22}) - collaborators1, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 1}) + collaborators1, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{ + ListOptions: db.ListOptions{PageSize: 1, Page: 1}, + RepoID: repo.ID, + }) assert.NoError(t, err) assert.Len(t, collaborators1, 1) - collaborators2, err := repo_model.GetCollaborators(db.DefaultContext, repo.ID, db.ListOptions{PageSize: 1, Page: 2}) + collaborators2, _, err := repo_model.GetCollaborators(db.DefaultContext, &repo_model.FindCollaborationOptions{ + ListOptions: db.ListOptions{PageSize: 1, Page: 2}, + RepoID: repo.ID, + }) assert.NoError(t, err) assert.Len(t, collaborators2, 1) @@ -85,31 +91,6 @@ func TestRepository_ChangeCollaborationAccessMode(t *testing.T) { unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) } -func TestRepository_CountCollaborators(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - count, err := db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{ - RepoID: repo1.ID, - }) - assert.NoError(t, err) - assert.EqualValues(t, 2, count) - - repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 22}) - count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{ - RepoID: repo2.ID, - }) - assert.NoError(t, err) - assert.EqualValues(t, 2, count) - - // Non-existent repository. - count, err = db.Count[repo_model.Collaboration](db.DefaultContext, repo_model.FindCollaborationOptions{ - RepoID: unittest.NonexistentID, - }) - assert.NoError(t, err) - assert.EqualValues(t, 0, count) -} - func TestRepository_IsOwnerMemberCollaborator(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/repo/git.go b/models/repo/git.go index 610c554296..388bf86522 100644 --- a/models/repo/git.go +++ b/models/repo/git.go @@ -21,6 +21,8 @@ const ( MergeStyleRebaseMerge MergeStyle = "rebase-merge" // MergeStyleSquash squash commits into single commit before merging MergeStyleSquash MergeStyle = "squash" + // MergeStyleFastForwardOnly fast-forward merge if possible, otherwise fail + MergeStyleFastForwardOnly MergeStyle = "fast-forward-only" // MergeStyleManuallyMerged pr has been merged manually, just mark it as merged directly MergeStyleManuallyMerged MergeStyle = "manually-merged" // MergeStyleRebaseUpdate not a merge style, used to update pull head by rebase diff --git a/models/repo/issue.go b/models/repo/issue.go index 6f6b565a00..0dd4fd5ed4 100644 --- a/models/repo/issue.go +++ b/models/repo/issue.go @@ -53,7 +53,7 @@ func (repo *Repository) IsDependenciesEnabled(ctx context.Context) bool { var u *RepoUnit var err error if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil { - log.Trace("%s", err) + log.Trace("IsDependenciesEnabled: %v", err) return setting.Service.DefaultEnableDependencies } return u.IssuesConfig().EnableDependencies diff --git a/models/repo/release.go b/models/repo/release.go index 1f37f11b2e..a9f65f6c3e 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -7,6 +7,7 @@ package repo import ( "context" "fmt" + "html/template" "net/url" "sort" "strconv" @@ -15,6 +16,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -79,7 +81,7 @@ type Release struct { NumCommits int64 NumCommitsBehind int64 `xorm:"-"` Note string `xorm:"TEXT"` - RenderedNote string `xorm:"-"` + RenderedNote template.HTML `xorm:"-"` IsDraft bool `xorm:"NOT NULL DEFAULT false"` IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases @@ -228,10 +230,10 @@ type FindReleasesOptions struct { RepoID int64 IncludeDrafts bool IncludeTags bool - IsPreRelease util.OptionalBool - IsDraft util.OptionalBool + IsPreRelease optional.Option[bool] + IsDraft optional.Option[bool] TagNames []string - HasSha1 util.OptionalBool // useful to find draft releases which are created with existing tags + HasSha1 optional.Option[bool] // useful to find draft releases which are created with existing tags } func (opts FindReleasesOptions) ToConds() builder.Cond { @@ -246,14 +248,14 @@ func (opts FindReleasesOptions) ToConds() builder.Cond { if len(opts.TagNames) > 0 { cond = cond.And(builder.In("tag_name", opts.TagNames)) } - if !opts.IsPreRelease.IsNone() { - cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()}) + if opts.IsPreRelease.Has() { + cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.Value()}) } - if !opts.IsDraft.IsNone() { - cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()}) + if opts.IsDraft.Has() { + cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()}) } - if !opts.HasSha1.IsNone() { - if opts.HasSha1.IsTrue() { + if opts.HasSha1.Has() { + if opts.HasSha1.Value() { cond = cond.And(builder.Neq{"sha1": ""}) } else { cond = cond.And(builder.Eq{"sha1": ""}) @@ -275,7 +277,7 @@ func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) { ListOptions: listOptions, IncludeDrafts: true, IncludeTags: true, - HasSha1: util.OptionalBoolTrue, + HasSha1: optional.Some(true), RepoID: repoID, } diff --git a/models/repo/repo.go b/models/repo/repo.go index 13493ba6e8..5d5707d1ac 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/git" "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/timeutil" @@ -135,6 +136,7 @@ type Repository struct { OriginalServiceType api.GitServiceType `xorm:"index"` OriginalURL string `xorm:"VARCHAR(2048)"` DefaultBranch string + DefaultWikiBranch string NumWatches int NumStars int @@ -284,6 +286,9 @@ func (repo *Repository) AfterLoad() { repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns + if repo.DefaultWikiBranch == "" { + repo.DefaultWikiBranch = setting.Repository.DefaultBranch + } } // LoadAttributes loads attributes of the repository. @@ -410,6 +415,13 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit Type: tp, Config: new(ActionsConfig), } + } else if tp == unit.TypeProjects { + cfg := new(ProjectsConfig) + cfg.ProjectsMode = ProjectsModeNone + return &RepoUnit{ + Type: tp, + Config: cfg, + } } return &RepoUnit{ @@ -519,6 +531,9 @@ func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) { return nil } + if repo.BaseRepo != nil { + return nil + } repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID) return err } @@ -840,7 +855,7 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository { type CountRepositoryOptions struct { OwnerID int64 - Private util.OptionalBool + Private optional.Option[bool] } // CountRepositories returns number of repositories. @@ -852,8 +867,8 @@ func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, if opts.OwnerID > 0 { sess.And("owner_id = ?", opts.OwnerID) } - if !opts.Private.IsNone() { - sess.And("is_private=?", opts.Private.IsTrue()) + if opts.Private.Has() { + sess.And("is_private=?", opts.Private.Value()) } count, err := sess.Count(new(Repository)) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 533ca5251f..987c7df9b0 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -62,24 +63,60 @@ func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList { return RepositoryList(ValuesRepository(repoMap)) } +func (repos RepositoryList) LoadUnits(ctx context.Context) error { + if len(repos) == 0 { + return nil + } + + // Load units. + units := make([]*RepoUnit, 0, len(repos)*6) + if err := db.GetEngine(ctx). + In("repo_id", repos.IDs()). + Find(&units); err != nil { + return fmt.Errorf("find units: %w", err) + } + + unitsMap := make(map[int64][]*RepoUnit, len(repos)) + for _, unit := range units { + if !unit.Type.UnitGlobalDisabled() { + unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit) + } + } + + for _, repo := range repos { + repo.Units = unitsMap[repo.ID] + } + + return nil +} + +func (repos RepositoryList) IDs() []int64 { + repoIDs := make([]int64, len(repos)) + for i := range repos { + repoIDs[i] = repos[i].ID + } + return repoIDs +} + // LoadAttributes loads the attributes for the given RepositoryList func (repos RepositoryList) LoadAttributes(ctx context.Context) error { if len(repos) == 0 { return nil } - set := make(container.Set[int64]) + userIDs := container.FilterSlice(repos, func(repo *Repository) (int64, bool) { + return repo.OwnerID, true + }) repoIDs := make([]int64, len(repos)) for i := range repos { - set.Add(repos[i].OwnerID) repoIDs[i] = repos[i].ID } // Load owners. - users := make(map[int64]*user_model.User, len(set)) + users := make(map[int64]*user_model.User, len(userIDs)) if err := db.GetEngine(ctx). Where("id > 0"). - In("id", set.Values()). + In("id", userIDs). Find(&users); err != nil { return fmt.Errorf("find users: %w", err) } @@ -125,11 +162,11 @@ type SearchRepoOptions struct { // None -> include public and private // True -> include just private // False -> include just public - IsPrivate util.OptionalBool + IsPrivate optional.Option[bool] // None -> include collaborative AND non-collaborative // True -> include just collaborative // False -> include just non-collaborative - Collaborate util.OptionalBool + Collaborate optional.Option[bool] // What type of unit the user can be collaborative in, // it is ignored if Collaborate is False. // TypeInvalid means any unit type. @@ -137,19 +174,19 @@ type SearchRepoOptions struct { // None -> include forks AND non-forks // True -> include just forks // False -> include just non-forks - Fork util.OptionalBool + Fork optional.Option[bool] // None -> include templates AND non-templates // True -> include just templates // False -> include just non-templates - Template util.OptionalBool + Template optional.Option[bool] // None -> include mirrors AND non-mirrors // True -> include just mirrors // False -> include just non-mirrors - Mirror util.OptionalBool + Mirror optional.Option[bool] // None -> include archived AND non-archived // True -> include just archived // False -> include just non-archived - Archived util.OptionalBool + Archived optional.Option[bool] // only search topic name TopicOnly bool // only search repositories with specified primary language @@ -159,7 +196,7 @@ type SearchRepoOptions struct { // None -> include has milestones AND has no milestone // True -> include just has milestones // False -> include just has no milestone - HasMilestones util.OptionalBool + HasMilestones optional.Option[bool] // LowerNames represents valid lower names to restrict to LowerNames []string // When specified true, apply some filters over the conditions: @@ -359,12 +396,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { ))) } - if opts.IsPrivate != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.IsTrue()}) + if opts.IsPrivate.Has() { + cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()}) } - if opts.Template != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue}) + if opts.Template.Has() { + cond = cond.And(builder.Eq{"is_template": opts.Template.Value()}) } // Restrict to starred repositories @@ -380,11 +417,11 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate if opts.OwnerID > 0 { accessCond := builder.NewCond() - if opts.Collaborate != util.OptionalBoolTrue { + if !opts.Collaborate.Value() { accessCond = builder.Eq{"owner_id": opts.OwnerID} } - if opts.Collaborate != util.OptionalBoolFalse { + if opts.Collaborate.ValueOrDefault(true) { // A Collaboration is: collaborateCond := builder.NewCond() @@ -472,31 +509,32 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true}))) } - if opts.Fork != util.OptionalBoolNone || opts.OnlyShowRelevant { - if opts.OnlyShowRelevant && opts.Fork == util.OptionalBoolNone { + if opts.Fork.Has() || opts.OnlyShowRelevant { + if opts.OnlyShowRelevant && !opts.Fork.Has() { cond = cond.And(builder.Eq{"is_fork": false}) } else { - cond = cond.And(builder.Eq{"is_fork": opts.Fork == util.OptionalBoolTrue}) + cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()}) } } - if opts.Mirror != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue}) + if opts.Mirror.Has() { + cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()}) } if opts.Actor != nil && opts.Actor.IsRestricted { cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid)) } - if opts.Archived != util.OptionalBoolNone { - cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue}) + if opts.Archived.Has() { + cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()}) } - switch opts.HasMilestones { - case util.OptionalBoolTrue: - cond = cond.And(builder.Gt{"num_milestones": 0}) - case util.OptionalBoolFalse: - cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) + if opts.HasMilestones.Has() { + if opts.HasMilestones.Value() { + cond = cond.And(builder.Gt{"num_milestones": 0}) + } else { + cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) + } } if opts.OnlyShowRelevant { diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 8a1799aac0..88cfcde620 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -10,7 +10,7 @@ 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/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -27,62 +27,62 @@ func getTestCases() []struct { }{ { name: "PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)}, count: 7, }, { name: "PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicAndPrivateRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicAndPrivateRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { @@ -117,33 +117,33 @@ func getTestCases() []struct { }, { name: "PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)}, count: 1, }, { name: "PublicAndPrivateRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)}, count: 2, }, { name: "AllPublic/PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)}, count: 7, }, { name: "AllPublic/PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, - count: 31, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, + count: 33, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse}, - count: 36, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, + count: 38, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", @@ -157,12 +157,12 @@ func getTestCases() []struct { }, { name: "AllPublic/PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, - count: 31, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, + count: 33, }, { name: "AllTemplates", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: util.OptionalBoolTrue}, + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)}, count: 2, }, { @@ -190,7 +190,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "repo_12", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -205,7 +205,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "test_repo", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -220,7 +220,7 @@ func TestSearchRepository(t *testing.T) { }, Keyword: "repo_13", Private: true, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -236,7 +236,7 @@ func TestSearchRepository(t *testing.T) { }, Keyword: "test_repo", Private: true, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) assert.NoError(t, err) @@ -257,7 +257,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "description_14", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), IncludeDescription: true, }) @@ -274,7 +274,7 @@ func TestSearchRepository(t *testing.T) { PageSize: 10, }, Keyword: "description_14", - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), IncludeDescription: false, }) @@ -327,30 +327,25 @@ func TestSearchRepository(t *testing.T) { assert.False(t, repo.IsPrivate) } - if testCase.opts.Fork == util.OptionalBoolTrue && testCase.opts.Mirror == util.OptionalBoolTrue { - assert.True(t, repo.IsFork || repo.IsMirror) + if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() { + assert.True(t, repo.IsFork && repo.IsMirror) } else { - switch testCase.opts.Fork { - case util.OptionalBoolFalse: - assert.False(t, repo.IsFork) - case util.OptionalBoolTrue: - assert.True(t, repo.IsFork) + if testCase.opts.Fork.Has() { + assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork) } - switch testCase.opts.Mirror { - case util.OptionalBoolFalse: - assert.False(t, repo.IsMirror) - case util.OptionalBoolTrue: - assert.True(t, repo.IsMirror) + if testCase.opts.Mirror.Has() { + assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror) } } if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic { - switch testCase.opts.Collaborate { - case util.OptionalBoolFalse: - assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) - case util.OptionalBoolTrue: - assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID) + if testCase.opts.Collaborate.Has() { + if testCase.opts.Collaborate.Value() { + assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID) + } else { + assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) + } } } } diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index ca9209d751..c13b698abf 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -12,17 +12,17 @@ import ( "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/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) var ( countRepospts = repo_model.CountRepositoryOptions{OwnerID: 10} - countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolFalse} - countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: util.OptionalBoolTrue} + countReposptsPublic = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(false)} + countReposptsPrivate = repo_model.CountRepositoryOptions{OwnerID: 10, Private: optional.Some(true)} ) func TestGetRepositoryCount(t *testing.T) { @@ -64,16 +64,17 @@ func TestRepoAPIURL(t *testing.T) { func TestWatchRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - const repoID = 3 - const userID = 2 - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, userID, repoID, false)) - unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repoID, UserID: userID}) - unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repoID}) + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) + + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Watch{RepoID: repo.ID, UserID: user.ID}) + unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: repo.ID}) } func TestMetas(t *testing.T) { diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 8a3ba1ee89..5a841f4d31 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -122,6 +122,7 @@ type PullRequestsConfig struct { AllowRebase bool AllowRebaseMerge bool AllowSquash bool + AllowFastForwardOnly bool AllowManualMerge bool AutodetectManualMerge bool AllowRebaseUpdate bool @@ -148,6 +149,7 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool { mergeStyle == MergeStyleRebase && cfg.AllowRebase || mergeStyle == MergeStyleRebaseMerge && cfg.AllowRebaseMerge || mergeStyle == MergeStyleSquash && cfg.AllowSquash || + mergeStyle == MergeStyleFastForwardOnly && cfg.AllowFastForwardOnly || mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge } @@ -200,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) { return json.Marshal(cfg) } +// ProjectsMode represents the projects enabled for a repository +type ProjectsMode string + +const ( + // ProjectsModeRepo allows only repo-level projects + ProjectsModeRepo ProjectsMode = "repo" + // ProjectsModeOwner allows only owner-level projects + ProjectsModeOwner ProjectsMode = "owner" + // ProjectsModeAll allows both kinds of projects + ProjectsModeAll ProjectsMode = "all" + // ProjectsModeNone doesn't allow projects + ProjectsModeNone ProjectsMode = "none" +) + +// ProjectsConfig describes projects config +type ProjectsConfig struct { + ProjectsMode ProjectsMode +} + +// FromDB fills up a ProjectsConfig from serialized format. +func (cfg *ProjectsConfig) FromDB(bs []byte) error { + return json.UnmarshalHandleDoubleEncode(bs, &cfg) +} + +// ToDB exports a ProjectsConfig to a serialized format. +func (cfg *ProjectsConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + +func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode { + if cfg.ProjectsMode != "" { + return cfg.ProjectsMode + } + + return ProjectsModeAll +} + +func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool { + projectsMode := cfg.GetProjectsMode() + + if m == ProjectsModeNone { + return true + } + + return projectsMode == m || projectsMode == ProjectsModeAll +} + // BeforeSet is invoked from XORM before setting the value of a field of this object. func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { @@ -215,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { r.Config = new(IssuesConfig) case unit.TypeActions: r.Config = new(ActionsConfig) - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages: + case unit.TypeProjects: + r.Config = new(ProjectsConfig) + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages: fallthrough default: r.Config = new(UnitConfig) @@ -263,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig { return r.Config.(*ActionsConfig) } +// ProjectsConfig returns config for unit.ProjectsConfig +func (r *RepoUnit) ProjectsConfig() *ProjectsConfig { + return r.Config.(*ProjectsConfig) +} + func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) { var tmpUnits []*RepoUnit if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil { diff --git a/models/repo/star.go b/models/repo/star.go index 60737149da..4c66855525 100644 --- a/models/repo/star.go +++ b/models/repo/star.go @@ -24,26 +24,30 @@ func init() { } // StarRepo or unstar repository. -func StarRepo(ctx context.Context, userID, repoID int64, star bool) error { +func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - staring := IsStaring(ctx, userID, repoID) + staring := IsStaring(ctx, doer.ID, repo.ID) if star { + if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) { + return user_model.ErrBlockedUser + } + if staring { return nil } - if err := db.Insert(ctx, &Star{UID: userID, RepoID: repoID}); err != nil { + if err := db.Insert(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repo.ID); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", doer.ID); err != nil { return err } } else { @@ -51,13 +55,13 @@ func StarRepo(ctx context.Context, userID, repoID int64, star bool) error { return nil } - if _, err := db.DeleteByBean(ctx, &Star{UID: userID, RepoID: repoID}); err != nil { + if _, err := db.DeleteByBean(ctx, &Star{UID: doer.ID, RepoID: repo.ID}); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repo.ID); err != nil { return err } - if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID); err != nil { + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", doer.ID); err != nil { return err } } diff --git a/models/repo/star_test.go b/models/repo/star_test.go index 62eac4e29a..aaac89d975 100644 --- a/models/repo/star_test.go +++ b/models/repo/star_test.go @@ -9,21 +9,24 @@ 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" ) func TestStarRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - const userID = 2 - const repoID = 1 - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false)) - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) } func TestIsStaring(t *testing.T) { @@ -54,17 +57,18 @@ func TestRepository_GetStargazers2(t *testing.T) { func TestClearRepoStars(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - const userID = 2 - const repoID = 1 - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, true)) - unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.StarRepo(db.DefaultContext, userID, repoID, false)) - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) - assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID)) - unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.StarRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repo.ID)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0}) assert.NoError(t, err) assert.Len(t, gazers, 0) diff --git a/models/repo/topic.go b/models/repo/topic.go index 79b13e320d..430a60f603 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -178,7 +178,7 @@ type FindTopicOptions struct { Keyword string } -func (opts *FindTopicOptions) toConds() builder.Cond { +func (opts *FindTopicOptions) ToConds() builder.Cond { cond := builder.NewCond() if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID}) @@ -191,29 +191,24 @@ func (opts *FindTopicOptions) toConds() builder.Cond { return cond } -// FindTopics retrieves the topics via FindTopicOptions -func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, error) { - sess := db.GetEngine(ctx).Select("topic.*").Where(opts.toConds()) +func (opts *FindTopicOptions) ToOrders() string { orderBy := "topic.repo_count DESC" if opts.RepoID > 0 { - sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result } - if opts.PageSize != 0 && opts.Page != 0 { - sess = db.SetSessionPagination(sess, opts) - } - topics := make([]*Topic, 0, 10) - total, err := sess.OrderBy(orderBy).FindAndCount(&topics) - return topics, total, err + return orderBy } -// CountTopics counts the number of topics matching the FindTopicOptions -func CountTopics(ctx context.Context, opts *FindTopicOptions) (int64, error) { - sess := db.GetEngine(ctx).Where(opts.toConds()) - if opts.RepoID > 0 { - sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") +func (opts *FindTopicOptions) ToJoins() []db.JoinFunc { + if opts.RepoID <= 0 { + return nil + } + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") + return nil + }, } - return sess.Count(new(Topic)) } // GetRepoTopicByName retrieves topic from name for a repo if it exist @@ -283,7 +278,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e // SaveTopics save topics to a repository func SaveTopics(ctx context.Context, repoID int64, topicNames ...string) error { - topics, _, err := FindTopics(ctx, &FindTopicOptions{ + topics, err := db.Find[Topic](ctx, &FindTopicOptions{ RepoID: repoID, }) if err != nil { diff --git a/models/repo/topic_test.go b/models/repo/topic_test.go index 2b609e6d66..1600896b6e 100644 --- a/models/repo/topic_test.go +++ b/models/repo/topic_test.go @@ -19,18 +19,18 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - topics, _, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{}) + topics, err := db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, total, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ + topics, total, err := db.FindAndCount[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ ListOptions: db.ListOptions{Page: 1, PageSize: 2}, }) assert.NoError(t, err) assert.Len(t, topics, 2) assert.EqualValues(t, 6, total) - topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ + topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ RepoID: 1, }) assert.NoError(t, err) @@ -38,11 +38,11 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, repo_model.SaveTopics(db.DefaultContext, 2, "golang")) repo2NrOfTopics := 1 - topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{}) + topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ + topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) @@ -55,11 +55,11 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 1, topic.RepoCount) - topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{}) + topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{}) assert.NoError(t, err) assert.Len(t, topics, totalNrOfTopics) - topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ + topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ RepoID: 2, }) assert.NoError(t, err) diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index dd2ef62201..6862247657 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/db" "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/container" api "code.gitea.io/gitea/modules/structs" @@ -15,47 +16,82 @@ import ( "xorm.io/builder" ) +type StarredReposOptions struct { + db.ListOptions + StarrerID int64 + RepoOwnerID int64 + IncludePrivate bool +} + +func (opts *StarredReposOptions) ToConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "star.uid": opts.StarrerID, + } + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.Eq{ + "repository.owner_id": opts.RepoOwnerID, + }) + } + if !opts.IncludePrivate { + cond = cond.And(builder.Eq{ + "repository.is_private": false, + }) + } + return cond +} + +func (opts *StarredReposOptions) ToJoins() []db.JoinFunc { + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "star", "`repository`.id=`star`.repo_id") + return nil + }, + } +} + // GetStarredRepos returns the repos starred by a particular user -func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) { - sess := db.GetEngine(ctx). - Where("star.uid=?", userID). - Join("LEFT", "star", "`repository`.id=`star`.repo_id") - if !private { - sess = sess.And("is_private=?", false) +func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Repository, error) { + return db.Find[Repository](ctx, opts) +} + +type WatchedReposOptions struct { + db.ListOptions + WatcherID int64 + RepoOwnerID int64 + IncludePrivate bool +} + +func (opts *WatchedReposOptions) ToConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "watch.user_id": opts.WatcherID, } - - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - - repos := make([]*Repository, 0, listOptions.PageSize) - return repos, sess.Find(&repos) + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.Eq{ + "repository.owner_id": opts.RepoOwnerID, + }) } + if !opts.IncludePrivate { + cond = cond.And(builder.Eq{ + "repository.is_private": false, + }) + } + return cond.And(builder.Neq{ + "watch.mode": WatchModeDont, + }) +} - repos := make([]*Repository, 0, 10) - return repos, sess.Find(&repos) +func (opts *WatchedReposOptions) ToJoins() []db.JoinFunc { + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "watch", "`repository`.id=`watch`.repo_id") + return nil + }, + } } // GetWatchedRepos returns the repos watched by a particular user -func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) { - sess := db.GetEngine(ctx). - Where("watch.user_id=?", userID). - And("`watch`.mode<>?", WatchModeDont). - Join("LEFT", "watch", "`repository`.id=`watch`.repo_id") - if !private { - sess = sess.And("is_private=?", false) - } - - if listOptions.Page != 0 { - sess = db.SetSessionPagination(sess, &listOptions) - - repos := make([]*Repository, 0, listOptions.PageSize) - total, err := sess.FindAndCount(&repos) - return repos, total, err - } - - repos := make([]*Repository, 0, 10) - total, err := sess.FindAndCount(&repos) - return repos, total, err +func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Repository, int64, error) { + return db.FindAndCount[Repository](ctx, opts) } // GetRepoAssignees returns all users that have write access and can be assigned to issues @@ -78,7 +114,8 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us if err = e.Table("team_user"). Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite). + Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", + repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). Distinct("`team_user`.uid"). Select("`team_user`.uid"). Find(&additionalUserIDs); err != nil { diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index 7816b0262a..591dcea5b5 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -25,10 +25,8 @@ func TestRepoAssignees(t *testing.T) { repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}) users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) assert.NoError(t, err) - assert.Len(t, users, 3) - assert.Equal(t, users[0].ID, int64(15)) - assert.Equal(t, users[1].ID, int64(18)) - assert.Equal(t, users[2].ID, int64(16)) + assert.Len(t, users, 4) + assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID}) } func TestRepoGetReviewers(t *testing.T) { diff --git a/models/repo/watch.go b/models/repo/watch.go index 80da4030cb..a616544cae 100644 --- a/models/repo/watch.go +++ b/models/repo/watch.go @@ -104,29 +104,23 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) return err } -// WatchRepoMode watch repository in specific mode. -func WatchRepoMode(ctx context.Context, userID, repoID int64, mode WatchMode) (err error) { - var watch Watch - if watch, err = GetWatch(ctx, userID, repoID); err != nil { - return err - } - return watchRepoMode(ctx, watch, mode) -} - // WatchRepo watch or unwatch repository. -func WatchRepo(ctx context.Context, userID, repoID int64, doWatch bool) (err error) { - var watch Watch - if watch, err = GetWatch(ctx, userID, repoID); err != nil { +func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doWatch bool) error { + watch, err := GetWatch(ctx, doer.ID, repo.ID) + if err != nil { return err } if !doWatch && watch.Mode == WatchModeAuto { - err = watchRepoMode(ctx, watch, WatchModeDont) + return watchRepoMode(ctx, watch, WatchModeDont) } else if !doWatch { - err = watchRepoMode(ctx, watch, WatchModeNone) - } else { - err = watchRepoMode(ctx, watch, WatchModeNormal) + return watchRepoMode(ctx, watch, WatchModeNone) } - return err + + if user_model.IsUserBlockedBy(ctx, doer, repo.OwnerID) { + return user_model.ErrBlockedUser + } + + return watchRepoMode(ctx, watch, WatchModeNormal) } // GetWatchers returns all watchers of given repository. diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index 7aa899291c..a95a267961 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_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" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -64,6 +65,8 @@ func TestWatchIfAuto(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user12 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12}) + watchers, err := repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, repo.NumWatches) @@ -105,7 +108,7 @@ func TestWatchIfAuto(t *testing.T) { assert.Len(t, watchers, prevCount+1) // Should remove watch, inhibit from adding auto - assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, 12, 1, false)) + assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, user12, repo, false)) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) assert.Len(t, watchers, prevCount) @@ -116,24 +119,3 @@ func TestWatchIfAuto(t *testing.T) { assert.NoError(t, err) assert.Len(t, watchers, prevCount) } - -func TestWatchRepoMode(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeAuto)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeAuto}, 1) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNormal)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeNormal}, 1) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeDont)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 1) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1, Mode: repo_model.WatchModeDont}, 1) - - assert.NoError(t, repo_model.WatchRepoMode(db.DefaultContext, 12, 1, repo_model.WatchModeNone)) - unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0) -} diff --git a/models/repo_transfer.go b/models/repo_transfer.go index 676e2dbb63..747ec2f248 100644 --- a/models/repo_transfer.go +++ b/models/repo_transfer.go @@ -13,6 +13,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" ) // RepoTransfer is used to manage repository transfers @@ -94,21 +96,46 @@ func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model. return allowed } +type PendingRepositoryTransferOptions struct { + RepoID int64 + SenderID int64 + RecipientID int64 +} + +func (opts *PendingRepositoryTransferOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.SenderID != 0 { + cond = cond.And(builder.Eq{"doer_id": opts.SenderID}) + } + if opts.RecipientID != 0 { + cond = cond.And(builder.Eq{"recipient_id": opts.RecipientID}) + } + return cond +} + +func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryTransferOptions) ([]*RepoTransfer, error) { + transfers := make([]*RepoTransfer, 0, 10) + return transfers, db.GetEngine(ctx). + Where(opts.ToConds()). + Find(&transfers) +} + // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer // process for the repository func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) { - transfer := new(RepoTransfer) - - has, err := db.GetEngine(ctx).Where("repo_id = ? ", repo.ID).Get(transfer) + transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID}) if err != nil { return nil, err } - if !has { + if len(transfers) != 1 { return nil, ErrNoPendingRepoTransfer{RepoID: repo.ID} } - return transfer, nil + return transfers[0], nil } func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error { diff --git a/models/secret/secret.go b/models/secret/secret.go index 41e860d7f6..35bed500b9 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -9,7 +9,10 @@ import ( "fmt" "strings" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/log" secret_module "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -112,3 +115,39 @@ func UpdateSecret(ctx context.Context, secretID int64, data string) error { } return err } + +func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) { + 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, nil + } + + ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID}) + if err != nil { + log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err) + return nil, err + } + repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: task.Job.Run.RepoID}) + if err != nil { + log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err) + return nil, err + } + + for _, secret := range append(ownerSecrets, repoSecrets...) { + v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data) + if err != nil { + log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err) + return nil, err + } + secrets[secret.Name] = v + } + + return secrets, nil +} diff --git a/models/shared/types/ownertype.go b/models/shared/types/ownertype.go index e6fe4e4cfd..a1d46c986f 100644 --- a/models/shared/types/ownertype.go +++ b/models/shared/types/ownertype.go @@ -17,13 +17,13 @@ const ( func (o OwnerType) LocaleString(locale translation.Locale) string { switch o { case OwnerTypeSystemGlobal: - return locale.Tr("concept_system_global") + return locale.TrString("concept_system_global") case OwnerTypeIndividual: - return locale.Tr("concept_user_individual") + return locale.TrString("concept_user_individual") case OwnerTypeRepository: - return locale.Tr("concept_code_repository") + return locale.TrString("concept_code_repository") case OwnerTypeOrganization: - return locale.Tr("concept_user_organization") + return locale.TrString("concept_user_organization") } - return locale.Tr("unknown") + return locale.TrString("unknown") } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 4c668ad04b..cb90c12f2b 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -44,12 +44,12 @@ func fatalTestError(fmtStr string, args ...any) { } // InitSettings initializes config provider and load common settings for tests -func InitSettings(extraConfigs ...string) { +func InitSettings() { if setting.CustomConf == "" { setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") _ = os.Remove(setting.CustomConf) } - setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n")) + setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() if err := setting.PrepareAppDataPath(); err != nil { diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index d47bceea1e..75898436fc 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -131,8 +131,8 @@ func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { } // AssertCount assert the count of a bean -func AssertCount(t assert.TestingT, bean, expected any) { - assert.EqualValues(t, expected, GetCount(t, bean)) +func AssertCount(t assert.TestingT, bean, expected any) bool { + return assert.EqualValues(t, expected, GetCount(t, bean)) } // AssertInt64InRange assert value is in range [low, high] @@ -150,7 +150,7 @@ func GetCountByCond(t assert.TestingT, tableName string, cond builder.Cond) int6 } // AssertCountByCond test the count of database entries matching bean -func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) { - assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), +func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, expected int) bool { + return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), "Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond) } diff --git a/models/user/badge.go b/models/user/badge.go index ee52b44cf5..3ff3530a36 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -5,13 +5,15 @@ package user import ( "context" + "fmt" "code.gitea.io/gitea/models/db" ) // Badge represents a user badge type Badge struct { - ID int64 `xorm:"pk autoincr"` + ID int64 `xorm:"pk autoincr"` + Slug string `xorm:"UNIQUE"` Description string ImageURL string } @@ -39,3 +41,84 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) { count, err := sess.FindAndCount(&badges) return badges, count, err } + +// CreateBadge creates a new badge. +func CreateBadge(ctx context.Context, badge *Badge) error { + _, err := db.GetEngine(ctx).Insert(badge) + return err +} + +// GetBadge returns a badge +func GetBadge(ctx context.Context, slug string) (*Badge, error) { + badge := new(Badge) + has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge) + if !has { + return nil, err + } + return badge, err +} + +// UpdateBadge updates a badge based on its slug. +func UpdateBadge(ctx context.Context, badge *Badge) error { + _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge) + return err +} + +// DeleteBadge deletes a badge. +func DeleteBadge(ctx context.Context, badge *Badge) error { + _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge) + return err +} + +// AddUserBadge adds a badge to a user. +func AddUserBadge(ctx context.Context, u *User, badge *Badge) error { + return AddUserBadges(ctx, u, []*Badge{badge}) +} + +// AddUserBadges adds badges to a user. +func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + for _, badge := range badges { + // hydrate badge and check if it exists + has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge) + if err != nil { + return err + } else if !has { + return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug) + } + if err := db.Insert(ctx, &UserBadge{ + BadgeID: badge.ID, + UserID: u.ID, + }); err != nil { + return err + } + } + return nil + }) +} + +// RemoveUserBadge removes a badge from a user. +func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error { + return RemoveUserBadges(ctx, u, []*Badge{badge}) +} + +// RemoveUserBadges removes badges from a user. +func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + for _, badge := range badges { + if _, err := db.GetEngine(ctx). + Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). + Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug). + Delete(&UserBadge{}); err != nil { + return err + } + } + return nil + }) +} + +// RemoveAllUserBadges removes all badges from a user. +func RemoveAllUserBadges(ctx context.Context, u *User) error { + _, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{}) + return err +} diff --git a/models/user/block.go b/models/user/block.go new file mode 100644 index 0000000000..5f2b65a199 --- /dev/null +++ b/models/user/block.go @@ -0,0 +1,123 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "context" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +var ( + ErrBlockOrganization = util.NewInvalidArgumentErrorf("cannot block an organization") + ErrCanNotBlock = util.NewInvalidArgumentErrorf("cannot block the user") + ErrCanNotUnblock = util.NewInvalidArgumentErrorf("cannot unblock the user") + ErrBlockedUser = util.NewPermissionDeniedErrorf("user is blocked") +) + +type Blocking struct { + ID int64 `xorm:"pk autoincr"` + BlockerID int64 `xorm:"UNIQUE(block)"` + Blocker *User `xorm:"-"` + BlockeeID int64 `xorm:"UNIQUE(block)"` + Blockee *User `xorm:"-"` + Note string + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +func (*Blocking) TableName() string { + return "user_blocking" +} + +func init() { + db.RegisterModel(new(Blocking)) +} + +func UpdateBlockingNote(ctx context.Context, id int64, note string) error { + _, err := db.GetEngine(ctx).ID(id).Cols("note").Update(&Blocking{Note: note}) + return err +} + +func IsUserBlockedBy(ctx context.Context, blockee *User, blockerIDs ...int64) bool { + if len(blockerIDs) == 0 { + return false + } + + if blockee.IsAdmin { + return false + } + + cond := builder.Eq{"user_blocking.blockee_id": blockee.ID}. + And(builder.In("user_blocking.blocker_id", blockerIDs)) + + has, _ := db.GetEngine(ctx).Where(cond).Exist(&Blocking{}) + return has +} + +type FindBlockingOptions struct { + db.ListOptions + BlockerID int64 + BlockeeID int64 +} + +func (opts *FindBlockingOptions) ToConds() builder.Cond { + cond := builder.NewCond() + if opts.BlockerID != 0 { + cond = cond.And(builder.Eq{"user_blocking.blocker_id": opts.BlockerID}) + } + if opts.BlockeeID != 0 { + cond = cond.And(builder.Eq{"user_blocking.blockee_id": opts.BlockeeID}) + } + return cond +} + +func FindBlockings(ctx context.Context, opts *FindBlockingOptions) ([]*Blocking, int64, error) { + return db.FindAndCount[Blocking](ctx, opts) +} + +func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, error) { + blocks, _, err := FindBlockings(ctx, &FindBlockingOptions{ + BlockerID: blockerID, + BlockeeID: blockeeID, + }) + if err != nil { + return nil, err + } + if len(blocks) == 0 { + return nil, nil + } + return blocks[0], nil +} + +type BlockingList []*Blocking + +func (blocks BlockingList) LoadAttributes(ctx context.Context) error { + ids := make(container.Set[int64], len(blocks)*2) + for _, b := range blocks { + ids.Add(b.BlockerID) + ids.Add(b.BlockeeID) + } + + userList, err := GetUsersByIDs(ctx, ids.Values()) + if err != nil { + return err + } + + userMap := make(map[int64]*User, len(userList)) + for _, u := range userList { + userMap[u.ID] = u + } + + for _, b := range blocks { + b.Blocker = userMap[b.BlockerID] + b.Blockee = userMap[b.BlockeeID] + } + + return nil +} diff --git a/models/user/email_address.go b/models/user/email_address.go index 957e72fe89..08771efe99 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/db" "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/util" "code.gitea.io/gitea/modules/validation" @@ -21,9 +22,6 @@ import ( "xorm.io/builder" ) -// ErrEmailNotActivated e-mail address has not been activated error -var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated") - // ErrEmailCharIsNotSupported e-mail address contains unsupported character type ErrEmailCharIsNotSupported struct { Email string @@ -156,37 +154,18 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") -// ValidateEmail check if email is a allowed address +// ValidateEmail check if email is a valid & allowed address func ValidateEmail(email string) error { - if len(email) == 0 { - return ErrEmailInvalid{email} + if err := validateEmailBasic(email); err != nil { + return err } + return validateEmailDomain(email) +} - if !emailRegexp.MatchString(email) { - return ErrEmailCharIsNotSupported{email} - } - - if email[0] == '-' { - return ErrEmailInvalid{email} - } - - if _, err := mail.ParseAddress(email); err != nil { - return ErrEmailInvalid{email} - } - - // if there is no allow list, then check email against block list - if len(setting.Service.EmailDomainAllowList) == 0 && - validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { - return ErrEmailInvalid{email} - } - - // if there is an allow list, then check email against allow list - if len(setting.Service.EmailDomainAllowList) > 0 && - !validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { - return ErrEmailInvalid{email} - } - - return nil +// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users +func ValidateEmailForAdmin(email string) error { + return validateEmailBasic(email) + // In this case we do not need to check the email domain } func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) { @@ -277,14 +256,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) { return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{}) } -// DeleteInactiveEmailAddresses deletes inactive email addresses -func DeleteInactiveEmailAddresses(ctx context.Context) error { - _, err := db.GetEngine(ctx). - Where("is_activated = ?", false). - Delete(new(EmailAddress)) - return err -} - // ActivateEmail activates the email address to given user. func ActivateEmail(ctx context.Context, email *EmailAddress) error { ctx, committer, err := db.TxContext(ctx) @@ -313,29 +284,27 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e return UpdateUserCols(ctx, user, "rands") } -// MakeEmailPrimary sets primary email address of given user. -func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error { - has, err := db.GetEngine(ctx).Get(email) - if err != nil { +func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error { + return makeEmailPrimaryInternal(ctx, emailID, true) +} + +func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error { + return makeEmailPrimaryInternal(ctx, emailID, false) +} + +func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error { + email := &EmailAddress{} + if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil { return err } else if !has { - return ErrEmailAddressNotExist{Email: email.Email} - } - - if !email.IsActivated { - return ErrEmailNotActivated + return ErrEmailAddressNotExist{} } user := &User{} - has, err = db.GetEngine(ctx).ID(email.UID).Get(user) - if err != nil { + if has, err := db.GetEngine(ctx).ID(email.UID).Get(user); err != nil { return err } else if !has { - return ErrUserNotExist{ - UID: email.UID, - Name: "", - KeyID: 0, - } + return ErrUserNotExist{UID: email.UID} } ctx, committer, err := db.TxContext(ctx) @@ -367,6 +336,21 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error { return committer.Commit() } +// ChangeInactivePrimaryEmail replaces the inactive primary email of a given user +func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, newEmailAddr string) error { + return db.WithTx(ctx, func(ctx context.Context) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{"uid": uid, "lower_email": strings.ToLower(oldEmailAddr)}).Delete(&EmailAddress{}) + if err != nil { + return err + } + newEmail, err := InsertEmailAddress(ctx, &EmailAddress{UID: uid, Email: newEmailAddr}) + if err != nil { + return err + } + return MakeInactiveEmailPrimary(ctx, newEmail.ID) + }) +} + // VerifyActiveEmailCode verifies active email code when active account func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress { minutes := setting.Service.ActiveCodeLives @@ -406,8 +390,8 @@ type SearchEmailOptions struct { db.ListOptions Keyword string SortType SearchEmailOrderBy - IsPrimary util.OptionalBool - IsActivated util.OptionalBool + IsPrimary optional.Option[bool] + IsActivated optional.Option[bool] } // SearchEmailResult is an e-mail address found in the user or email_address table @@ -434,21 +418,15 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail )) } - switch { - case opts.IsPrimary.IsTrue(): - cond = cond.And(builder.Eq{"email_address.is_primary": true}) - case opts.IsPrimary.IsFalse(): - cond = cond.And(builder.Eq{"email_address.is_primary": false}) + if opts.IsPrimary.Has() { + cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()}) } - switch { - case opts.IsActivated.IsTrue(): - cond = cond.And(builder.Eq{"email_address.is_activated": true}) - case opts.IsActivated.IsFalse(): - cond = cond.And(builder.Eq{"email_address.is_activated": false}) + if opts.IsActivated.Has() { + cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()}) } - count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid"). + count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.id = email_address.uid"). Where(cond).Count(new(EmailAddress)) if err != nil { return nil, 0, fmt.Errorf("Count: %w", err) @@ -464,7 +442,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail emails := make([]*SearchEmailResult, 0, opts.PageSize) err = db.GetEngine(ctx).Table("email_address"). Select("email_address.*, `user`.name, `user`.full_name"). - Join("INNER", "`user`", "`user`.ID = email_address.uid"). + Join("INNER", "`user`", "`user`.id = email_address.uid"). Where(cond). OrderBy(orderby). Limit(opts.PageSize, (opts.Page-1)*opts.PageSize). @@ -529,3 +507,41 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate return committer.Commit() } + +// validateEmailBasic checks whether the email complies with the rules +func validateEmailBasic(email string) error { + if len(email) == 0 { + return ErrEmailInvalid{email} + } + + if !emailRegexp.MatchString(email) { + return ErrEmailCharIsNotSupported{email} + } + + if email[0] == '-' { + return ErrEmailInvalid{email} + } + + if _, err := mail.ParseAddress(email); err != nil { + return ErrEmailInvalid{email} + } + + return nil +} + +// validateEmailDomain checks whether the email domain is allowed or blocked +func validateEmailDomain(email string) error { + if !IsEmailDomainAllowed(email) { + return ErrEmailInvalid{email} + } + + return nil +} + +func IsEmailDomainAllowed(email string) bool { + if len(setting.Service.EmailDomainAllowList) == 0 { + return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) + } + + return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) +} diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 140443f82f..c2e010d95b 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" "github.com/stretchr/testify/assert" ) @@ -45,31 +45,22 @@ func TestIsEmailUsed(t *testing.T) { func TestMakeEmailPrimary(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - email := &user_model.EmailAddress{ - Email: "user567890@example.com", - } - err := user_model.MakeEmailPrimary(db.DefaultContext, email) + err := user_model.MakeActiveEmailPrimary(db.DefaultContext, 9999999) assert.Error(t, err) - assert.EqualError(t, err, user_model.ErrEmailAddressNotExist{Email: email.Email}.Error()) + assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) - email = &user_model.EmailAddress{ - Email: "user11@example.com", - } - err = user_model.MakeEmailPrimary(db.DefaultContext, email) + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"}) + err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID) assert.Error(t, err) - assert.EqualError(t, err, user_model.ErrEmailNotActivated.Error()) + assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary" - email = &user_model.EmailAddress{ - Email: "user9999999@example.com", - } - err = user_model.MakeEmailPrimary(db.DefaultContext, email) + email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"}) + err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID) assert.Error(t, err) assert.True(t, user_model.IsErrUserNotExist(err)) - email = &user_model.EmailAddress{ - Email: "user101@example.com", - } - err = user_model.MakeEmailPrimary(db.DefaultContext, email) + email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"}) + err = user_model.MakeActiveEmailPrimary(db.DefaultContext, email.ID) assert.NoError(t, err) user, _ := user_model.GetUserByID(db.DefaultContext, int64(10)) @@ -137,14 +128,14 @@ func TestListEmails(t *testing.T) { assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 27 })) // Must find only primary addresses (i.e. from the `user` table) - opts = &user_model.SearchEmailOptions{IsPrimary: util.OptionalBoolTrue} + opts = &user_model.SearchEmailOptions{IsPrimary: optional.Some(true)} emails, _, err = user_model.SearchEmails(db.DefaultContext, opts) assert.NoError(t, err) assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.IsPrimary })) assert.False(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsPrimary })) // Must find only inactive addresses (i.e. not validated) - opts = &user_model.SearchEmailOptions{IsActivated: util.OptionalBoolFalse} + opts = &user_model.SearchEmailOptions{IsActivated: optional.Some(false)} emails, _, err = user_model.SearchEmails(db.DefaultContext, opts) assert.NoError(t, err) assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return !s.IsActivated })) diff --git a/models/user/error.go b/models/user/error.go index ef572c178a..cbf19998d1 100644 --- a/models/user/error.go +++ b/models/user/error.go @@ -31,9 +31,8 @@ func (err ErrUserAlreadyExist) Unwrap() error { // ErrUserNotExist represents a "UserNotExist" kind of error. type ErrUserNotExist struct { - UID int64 - Name string - KeyID int64 + UID int64 + Name string } // IsErrUserNotExist checks if an error is a ErrUserNotExist. @@ -43,7 +42,7 @@ func IsErrUserNotExist(err error) bool { } func (err ErrUserNotExist) Error() string { - return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID) + return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name) } // Unwrap unwraps this error as a ErrNotExist error diff --git a/models/user/follow.go b/models/user/follow.go index f4dd2891ff..cf9672109a 100644 --- a/models/user/follow.go +++ b/models/user/follow.go @@ -29,26 +29,30 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool { } // FollowUser marks someone be another's follower. -func FollowUser(ctx context.Context, userID, followID int64) (err error) { - if userID == followID || IsFollowing(ctx, userID, followID) { +func FollowUser(ctx context.Context, user, follow *User) (err error) { + if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) { return nil } + if IsUserBlockedBy(ctx, user, follow.ID) || IsUserBlockedBy(ctx, follow, user.ID) { + return ErrBlockedUser + } + ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - if err = db.Insert(ctx, &Follow{UserID: userID, FollowID: followID}); err != nil { + if err = db.Insert(ctx, &Follow{UserID: user.ID, FollowID: follow.ID}); err != nil { return err } - if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil { + if _, err = db.Exec(ctx, "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", follow.ID); err != nil { return err } - if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil { + if _, err = db.Exec(ctx, "UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", user.ID); err != nil { return err } return committer.Commit() diff --git a/models/user/search.go b/models/user/search.go index 0fa278c257..45b051187e 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -9,8 +9,9 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm" @@ -30,11 +31,13 @@ type SearchUserOptions struct { Actor *User // The user doing the search SearchByEmail bool // Search by email as well as username/full name - IsActive util.OptionalBool - IsAdmin util.OptionalBool - IsRestricted util.OptionalBool - IsTwoFactorEnabled util.OptionalBool - IsProhibitLogin util.OptionalBool + SupportedSortOrders container.Set[string] // if not nil, only allow to use the sort orders in this set + + IsActive optional.Option[bool] + IsAdmin optional.Option[bool] + IsRestricted optional.Option[bool] + IsTwoFactorEnabled optional.Option[bool] + IsProhibitLogin optional.Option[bool] IncludeReserved bool ExtraParamStrings map[string]string @@ -86,24 +89,24 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess cond = cond.And(builder.Eq{"login_name": opts.LoginName}) } - if !opts.IsActive.IsNone() { - cond = cond.And(builder.Eq{"is_active": opts.IsActive.IsTrue()}) + if opts.IsActive.Has() { + cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()}) } - if !opts.IsAdmin.IsNone() { - cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()}) + if opts.IsAdmin.Has() { + cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) } - if !opts.IsRestricted.IsNone() { - cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.IsTrue()}) + if opts.IsRestricted.Has() { + cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()}) } - if !opts.IsProhibitLogin.IsNone() { - cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.IsTrue()}) + if opts.IsProhibitLogin.Has() { + cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()}) } e := db.GetEngine(ctx) - if opts.IsTwoFactorEnabled.IsNone() { + if !opts.IsTwoFactorEnabled.Has() { return e.Where(cond) } @@ -111,7 +114,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess // While using LEFT JOIN, sometimes the performance might not be good, but it won't be a problem now, such SQL is seldom executed. // There are some possible methods to refactor this SQL in future when we really need to optimize the performance (but not now): // (1) add a column in user table (2) add a setting value in user_setting table (3) use search engines (bleve/elasticsearch) - if opts.IsTwoFactorEnabled.IsTrue() { + if opts.IsTwoFactorEnabled.Value() { cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL")) } else { cond = cond.And(builder.Expr("two_factor.uid IS NULL")) @@ -128,7 +131,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ defer sessCount.Close() count, err := sessCount.Count(new(User)) if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) + return nil, 0, fmt.Errorf("count: %w", err) } if len(opts.OrderBy) == 0 { diff --git a/models/user/user.go b/models/user/user.go index e5245dfbb0..d459ec239e 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "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/timeutil" @@ -424,15 +425,15 @@ func (u *User) GetDisplayName() string { return u.Name } -// GetCompleteName returns the the full name and username in the form of -// "Full Name (@username)" if full name is not empty, otherwise it returns -// "@username". +// GetCompleteName returns the full name and username in the form of +// "Full Name (username)" if full name is not empty, otherwise it returns +// "username". func (u *User) GetCompleteName() string { trimmedFullName := strings.TrimSpace(u.FullName) if len(trimmedFullName) > 0 { - return fmt.Sprintf("%s (@%s)", trimmedFullName, u.Name) + return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name) } - return fmt.Sprintf("@%s", u.Name) + return u.Name } func gitSafeName(name string) string { @@ -573,18 +574,28 @@ func IsUsableUsername(name string) error { // CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation type CreateUserOverwriteOptions struct { - KeepEmailPrivate util.OptionalBool + KeepEmailPrivate optional.Option[bool] Visibility *structs.VisibleType - AllowCreateOrganization util.OptionalBool + AllowCreateOrganization optional.Option[bool] EmailNotificationsPreference *string MaxRepoCreation *int Theme *string - IsRestricted util.OptionalBool - IsActive util.OptionalBool + IsRestricted optional.Option[bool] + IsActive optional.Option[bool] } // CreateUser creates record of a new user. func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, false, overwriteDefault...) +} + +// AdminCreateUser is used by admins to manually create users +func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { + return createUser(ctx, u, true, overwriteDefault...) +} + +// createUser creates record of a new user. +func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { if err = IsUsableUsername(u.Name); err != nil { return err } @@ -607,14 +618,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve // overwrite defaults if set if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { overwrite := overwriteDefault[0] - if !overwrite.KeepEmailPrivate.IsNone() { - u.KeepEmailPrivate = overwrite.KeepEmailPrivate.IsTrue() + if overwrite.KeepEmailPrivate.Has() { + u.KeepEmailPrivate = overwrite.KeepEmailPrivate.Value() } if overwrite.Visibility != nil { u.Visibility = *overwrite.Visibility } - if !overwrite.AllowCreateOrganization.IsNone() { - u.AllowCreateOrganization = overwrite.AllowCreateOrganization.IsTrue() + if overwrite.AllowCreateOrganization.Has() { + u.AllowCreateOrganization = overwrite.AllowCreateOrganization.Value() } if overwrite.EmailNotificationsPreference != nil { u.EmailNotificationsPreference = *overwrite.EmailNotificationsPreference @@ -625,11 +636,11 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve if overwrite.Theme != nil { u.Theme = *overwrite.Theme } - if !overwrite.IsRestricted.IsNone() { - u.IsRestricted = overwrite.IsRestricted.IsTrue() + if overwrite.IsRestricted.Has() { + u.IsRestricted = overwrite.IsRestricted.Value() } - if !overwrite.IsActive.IsNone() { - u.IsActive = overwrite.IsActive.IsTrue() + if overwrite.IsActive.Has() { + u.IsActive = overwrite.IsActive.Value() } } @@ -638,8 +649,14 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve return err } - if err := ValidateEmail(u.Email); err != nil { - return err + if createdByAdmin { + if err := ValidateEmailForAdmin(u.Email); err != nil { + return err + } + } else { + if err := ValidateEmail(u.Email); err != nil { + return err + } } ctx, committer, err := db.TxContext(ctx) @@ -714,7 +731,7 @@ func CreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUserOve // IsLastAdminUser check whether user is the last admin func IsLastAdminUser(ctx context.Context, user *User) bool { - if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: util.OptionalBoolTrue}) <= 1 { + if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 { return true } return false @@ -723,7 +740,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool { // CountUserFilter represent optional filters for CountUsers type CountUserFilter struct { LastLoginSince *int64 - IsAdmin util.OptionalBool + IsAdmin optional.Option[bool] } // CountUsers returns number of users. @@ -741,8 +758,8 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 { cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince}) } - if !opts.IsAdmin.IsNone() { - cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.IsTrue()}) + if opts.IsAdmin.Has() { + cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) } } @@ -835,7 +852,7 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) { if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist{id, "", 0} + return nil, ErrUserNotExist{UID: id} } return u, nil } @@ -885,14 +902,14 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { // GetUserByNameCtx returns user by given name. func GetUserByName(ctx context.Context, name string) (*User, error) { if len(name) == 0 { - return nil, ErrUserNotExist{0, name, 0} + return nil, ErrUserNotExist{Name: name} } u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual} has, err := db.GetEngine(ctx).Get(u) if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist{0, name, 0} + return nil, ErrUserNotExist{Name: name} } return u, nil } @@ -1033,7 +1050,7 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) [] // GetUserByEmail returns the user object by given e-mail if exists. func GetUserByEmail(ctx context.Context, email string) (*User, error) { if len(email) == 0 { - return nil, ErrUserNotExist{0, email, 0} + return nil, ErrUserNotExist{Name: email} } email = strings.ToLower(email) @@ -1060,7 +1077,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { } } - return nil, ErrUserNotExist{0, email, 0} + return nil, ErrUserNotExist{Name: email} } // GetUser checks if a user already exists @@ -1071,7 +1088,7 @@ func GetUser(ctx context.Context, user *User) (bool, error) { // GetUserByOpenID returns the user object by given OpenID if exists. func GetUserByOpenID(ctx context.Context, uri string) (*User, error) { if len(uri) == 0 { - return nil, ErrUserNotExist{0, uri, 0} + return nil, ErrUserNotExist{Name: uri} } uri, err := openid.Normalize(uri) @@ -1091,7 +1108,7 @@ func GetUserByOpenID(ctx context.Context, uri string) (*User, error) { return GetUserByID(ctx, oid.UID) } - return nil, ErrUserNotExist{0, uri, 0} + return nil, ErrUserNotExist{Name: uri} } // GetAdminUser returns the first administrator @@ -1166,7 +1183,7 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool { return false } - // If they follow - they see each over + // If they follow - they see each other follower := IsFollowing(ctx, u.ID, viewer.ID) if follower { return true @@ -1215,3 +1232,21 @@ func GetOrderByName() string { } return "name" } + +// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the +// user if applicable +func IsFeatureDisabledWithLoginType(user *User, feature string) bool { + // NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType + return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) || + setting.Admin.UserDisabledFeatures.Contains(feature) +} + +// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type +// of the user if applicable +func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { + // NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType + if user != nil && user.LoginType > auth.Plain { + return &setting.Admin.ExternalUserDisableFeatures + } + return &setting.Admin.UserDisabledFeatures +} diff --git a/models/user/user_test.go b/models/user/user_test.go index f3e5a95b1e..a4550fa655 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -16,10 +16,11 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password/hash" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -89,7 +90,7 @@ func TestSearchUsers(t *testing.T) { []int64{19, 25}) testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}}, - []int64{26}) + []int64{26, 41}) testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 5, PageSize: 2}}, []int64{}) @@ -101,31 +102,31 @@ func TestSearchUsers(t *testing.T) { } testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, - []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37}) + []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolFalse}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)}, []int64{9}) - testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, - []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37}) + testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, + []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) // order by name asc default - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)}, []int64{1}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)}, []int64{29}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)}, []int64{37}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: util.OptionalBoolTrue}, + testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, []int64{24}) } @@ -399,14 +400,19 @@ func TestGetUserByOpenID(t *testing.T) { func TestFollowUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(followerID, followedID int64) { - assert.NoError(t, user_model.FollowUser(db.DefaultContext, followerID, followedID)) - unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: followerID, FollowID: followedID}) + testSuccess := func(follower, followed *user_model.User) { + assert.NoError(t, user_model.FollowUser(db.DefaultContext, follower, followed)) + unittest.AssertExistsAndLoadBean(t, &user_model.Follow{UserID: follower.ID, FollowID: followed.ID}) } - testSuccess(4, 2) - testSuccess(5, 2) - assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2)) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + testSuccess(user4, user2) + testSuccess(user5, user2) + + assert.NoError(t, user_model.FollowUser(db.DefaultContext, user2, user2)) unittest.CheckConsistencyFor(t, &user_model.User{}) } @@ -521,3 +527,37 @@ func Test_NormalizeUserFromEmail(t *testing.T) { } } } + +func TestDisabledUserFeatures(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testValues := container.SetOf(setting.UserFeatureDeletion, + setting.UserFeatureManageSSHKeys, + setting.UserFeatureManageGPGKeys) + + oldSetting := setting.Admin.ExternalUserDisableFeatures + defer func() { + setting.Admin.ExternalUserDisableFeatures = oldSetting + }() + setting.Admin.ExternalUserDisableFeatures = testValues + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0) + + // no features should be disabled with a plain login type + assert.LessOrEqual(t, user.LoginType, auth.Plain) + assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0) + for _, f := range testValues.Values() { + assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f)) + } + + // check disabled features with external login type + user.LoginType = auth.OAuth2 + + // all features should be disabled + assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values()) + for _, f := range testValues.Values() { + assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) + } +} diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index 2fb655ebca..ff3fdbadb2 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -5,13 +5,13 @@ package webhook import ( "context" + "errors" "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -31,6 +31,7 @@ type HookRequest struct { URL string `json:"url"` HTTPMethod string `json:"http_method"` Headers map[string]string `json:"headers"` + Body string `json:"body"` } // HookResponse represents hook task response information. @@ -45,11 +46,15 @@ type HookTask struct { ID int64 `xorm:"pk autoincr"` HookID int64 `xorm:"index"` UUID string `xorm:"unique"` - api.Payloader `xorm:"-"` PayloadContent string `xorm:"LONGTEXT"` - EventType webhook_module.HookEventType - IsDelivered bool - Delivered timeutil.TimeStampNano + // PayloadVersion number to allow for smooth version upgrades: + // - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL + // - PayloadVersion 2: PayloadContent contains the original event + PayloadVersion int `xorm:"DEFAULT 1"` + + EventType webhook_module.HookEventType + IsDelivered bool + Delivered timeutil.TimeStampNano // History info. IsSucceed bool @@ -115,16 +120,12 @@ func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error) // it handles conversion from Payload to PayloadContent. func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) { t.UUID = gouuid.New().String() - if t.Payloader != nil { - data, err := t.Payloader.JSONPayload() - if err != nil { - return nil, err - } - t.PayloadContent = string(data) - } if t.Delivered == 0 { t.Delivered = timeutil.TimeStampNanoNow() } + if t.PayloadVersion == 0 { + return nil, errors.New("missing HookTask.PayloadVersion") + } return t, db.Insert(ctx, t) } @@ -165,6 +166,7 @@ func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, HookID: task.HookID, PayloadContent: task.PayloadContent, EventType: task.EventType, + PayloadVersion: task.PayloadVersion, }) } diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 4a84a3d411..894357e36a 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -433,7 +434,7 @@ type ListWebhookOptions struct { db.ListOptions RepoID int64 OwnerID int64 - IsActive util.OptionalBool + IsActive optional.Option[bool] } func (opts ListWebhookOptions) ToConds() builder.Cond { @@ -444,8 +445,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond { if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) } - if !opts.IsActive.IsNone() { - cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) + if opts.IsActive.Has() { + cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()}) } return cond } diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go index 2e89f9547b..a2a9ee321a 100644 --- a/models/webhook/webhook_system.go +++ b/models/webhook/webhook_system.go @@ -8,7 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/optional" ) // GetDefaultWebhooks returns all admin-default webhooks. @@ -34,15 +34,15 @@ func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) } // GetSystemWebhooks returns all admin system webhooks. -func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webhook, error) { +func GetSystemWebhooks(ctx context.Context, isActive optional.Option[bool]) ([]*Webhook, error) { webhooks := make([]*Webhook, 0, 5) - if isActive.IsNone() { + if !isActive.Has() { return webhooks, db.GetEngine(ctx). Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true). Find(&webhooks) } return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). + Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.Value()). Find(&webhooks) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index 694fd7a873..f4403776ce 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -11,9 +11,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/json" - api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" @@ -35,8 +34,10 @@ func TestWebhook_History(t *testing.T) { webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1}) tasks, err := webhook.History(db.DefaultContext, 0) assert.NoError(t, err) - if assert.Len(t, tasks, 1) { - assert.Equal(t, int64(1), tasks[0].ID) + if assert.Len(t, tasks, 3) { + assert.Equal(t, int64(3), tasks[0].ID) + assert.Equal(t, int64(2), tasks[1].ID) + assert.Equal(t, int64(1), tasks[2].ID) } webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) @@ -123,7 +124,7 @@ func TestGetWebhookByOwnerID(t *testing.T) { func TestGetActiveWebhooksByRepoID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: util.OptionalBoolTrue}) + hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(1), hooks[0].ID) @@ -143,7 +144,7 @@ func TestGetWebhooksByRepoID(t *testing.T) { func TestGetActiveWebhooksByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue}) + hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) @@ -197,8 +198,10 @@ func TestHookTasks(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTasks, err := HookTasks(db.DefaultContext, 1, 1) assert.NoError(t, err) - if assert.Len(t, hookTasks, 1) { - assert.Equal(t, int64(1), hookTasks[0].ID) + if assert.Len(t, hookTasks, 3) { + assert.Equal(t, int64(3), hookTasks[0].ID) + assert.Equal(t, int64(2), hookTasks[1].ID) + assert.Equal(t, int64(1), hookTasks[2].ID) } hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1) @@ -209,8 +212,8 @@ func TestHookTasks(t *testing.T) { func TestCreateHookTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, + HookID: 3, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -232,10 +235,10 @@ func TestUpdateHookTask(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNanoNow(), + HookID: 3, + IsDelivered: true, + Delivered: timeutil.TimeStampNanoNow(), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -249,9 +252,9 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: false, + HookID: 4, + IsDelivered: false, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -265,10 +268,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNanoNow(), + HookID: 4, + IsDelivered: true, + Delivered: timeutil.TimeStampNanoNow(), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -282,10 +285,10 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 3, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), + HookID: 3, + IsDelivered: true, + Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -299,9 +302,9 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: false, + HookID: 4, + IsDelivered: false, + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) @@ -315,10 +318,10 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - HookID: 4, - Payloader: &api.PushPayload{}, - IsDelivered: true, - Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), + HookID: 4, + IsDelivered: true, + Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()), + PayloadVersion: 2, } unittest.AssertNotExistsBean(t, hookTask) _, err := CreateHookTask(db.DefaultContext, hookTask) diff --git a/modules/actions/github.go b/modules/actions/github.go index fafea4e11a..68116ec83a 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -25,6 +25,45 @@ const ( GithubEventSchedule = "schedule" ) +// IsDefaultBranchWorkflow returns true if the event only triggers workflows on the default branch +func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool { + switch triggedEvent { + case webhook_module.HookEventDelete: + // GitHub "delete" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete + return true + case webhook_module.HookEventFork: + // GitHub "fork" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#fork + return true + case webhook_module.HookEventIssueComment: + // GitHub "issue_comment" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment + return true + case webhook_module.HookEventPullRequestComment: + // GitHub "pull_request_comment" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment + return true + case webhook_module.HookEventWiki: + // GitHub "gollum" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum + return true + case webhook_module.HookEventSchedule: + // GitHub "schedule" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule + return true + case webhook_module.HookEventIssues, + webhook_module.HookEventIssueAssign, + webhook_module.HookEventIssueLabel, + webhook_module.HookEventIssueMilestone: + // Github "issues" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues + return true + } + + return false +} + // canGithubEventMatch check if the input Github event can match any Gitea event. func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEventType) bool { switch eventName { @@ -52,7 +91,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestAssign, - webhook_module.HookEventPullRequestLabel: + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: return true default: @@ -73,6 +114,11 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent case GithubEventSchedule: return triggedEvent == webhook_module.HookEventSchedule + case GithubEventIssueComment: + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment + return triggedEvent == webhook_module.HookEventIssueComment || + triggedEvent == webhook_module.HookEventPullRequestComment + default: return eventName == string(triggedEvent) } diff --git a/modules/actions/github_test.go b/modules/actions/github_test.go index 4bf55ae03f..6652ff6eac 100644 --- a/modules/actions/github_test.go +++ b/modules/actions/github_test.go @@ -103,6 +103,12 @@ func TestCanGithubEventMatch(t *testing.T) { webhook_module.HookEventCreate, true, }, + { + "create pull request comment", + GithubEventIssueComment, + webhook_module.HookEventPullRequestComment, + true, + }, } for _, tc := range testCases { diff --git a/modules/actions/log.go b/modules/actions/log.go index cdf18646aa..c38082b5dc 100644 --- a/modules/actions/log.go +++ b/modules/actions/log.go @@ -100,7 +100,7 @@ func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limi } if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("scan: %w", err) + return nil, fmt.Errorf("ReadLogs scan: %w", err) } return rows, nil diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go index cbbc0b357d..31a74be3fd 100644 --- a/modules/actions/task_state.go +++ b/modules/actions/task_state.go @@ -35,9 +35,18 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { } else if task.Status.IsDone() { preStep.Stopped = task.Stopped preStep.Status = actions_model.StatusFailure + if task.Status.IsSkipped() { + preStep.Status = actions_model.StatusSkipped + } } logIndex += preStep.LogLength + // lastHasRunStep is the last step that has run. + // For example, + // 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1. + // 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3. + // 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2. + // So its Stopped is the Started of postStep when there are no more steps to run. var lastHasRunStep *actions_model.ActionTaskStep for _, step := range task.Steps { if step.Status.HasRun() { @@ -53,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { Name: postStepName, Status: actions_model.StatusWaiting, } - if task.Status.IsDone() { + // If the lastHasRunStep is the last step, or it has failed, postStep has started. + if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] { postStep.LogIndex = logIndex postStep.LogLength = task.LogLength - postStep.LogIndex - postStep.Status = task.Status postStep.Started = lastHasRunStep.Stopped + postStep.Status = actions_model.StatusRunning + } + if task.Status.IsDone() { + postStep.Status = task.Status postStep.Stopped = task.Stopped } ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2) diff --git a/modules/actions/task_state_test.go b/modules/actions/task_state_test.go index 3a599fbcbd..28213d781b 100644 --- a/modules/actions/task_state_test.go +++ b/modules/actions/task_state_test.go @@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) { {Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100}, }, }, + { + name: "all steps finished but task is running", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, + }, + Status: actions_model.StatusRunning, + Started: 10000, + Stopped: 0, + LogLength: 100, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010}, + {Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, + {Name: postStepName, Status: actions_model.StatusRunning, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 0}, + }, + }, + { + name: "skipped task", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{ + {Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + Status: actions_model.StatusSkipped, + Started: 0, + Stopped: 0, + LogLength: 0, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + {Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + {Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index a883f4181b..595fd8bbb0 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -221,7 +221,9 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestAssign, - webhook_module.HookEventPullRequestLabel: + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt) case // pull_request_review @@ -397,13 +399,13 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa } else { // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request // Actions with the same name: - // opened, edited, closed, reopened, assigned, unassigned + // opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned // Actions need to be converted: // synchronized -> synchronize // label_updated -> labeled // label_cleared -> unlabeled // Unsupported activity types: - // converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled + // converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued action := prPayload.Action switch action { @@ -439,6 +441,9 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa // all acts conditions should be satisfied for cond, vals := range acts { switch cond { + case "types": + // types have been checked + continue case "branches": refName := git.RefName(prPayload.PullRequest.Base.Ref) patterns, err := workflowpattern.CompilePatterns(vals...) diff --git a/modules/auth/password/hash/pbkdf2.go b/modules/auth/password/hash/pbkdf2.go index 9ff6d162fc..27382fedb8 100644 --- a/modules/auth/password/hash/pbkdf2.go +++ b/modules/auth/password/hash/pbkdf2.go @@ -4,12 +4,12 @@ package hash import ( + "crypto/sha256" "encoding/hex" "strings" "code.gitea.io/gitea/modules/log" - "github.com/minio/sha256-simd" "golang.org/x/crypto/pbkdf2" ) diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index 2c7205b708..27074358a9 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -8,6 +8,7 @@ import ( "context" "crypto/rand" "errors" + "html/template" "math/big" "strings" "sync" @@ -121,15 +122,15 @@ func Generate(n int) (string, error) { } // BuildComplexityError builds the error message when password complexity checks fail -func BuildComplexityError(locale translation.Locale) string { +func BuildComplexityError(locale translation.Locale) template.HTML { var buffer bytes.Buffer - buffer.WriteString(locale.Tr("form.password_complexity")) + buffer.WriteString(locale.TrString("form.password_complexity")) buffer.WriteString("<ul>") for _, c := range requiredList { buffer.WriteString("<li>") - buffer.WriteString(locale.Tr(c.TrNameOne)) + buffer.WriteString(locale.TrString(c.TrNameOne)) buffer.WriteString("</li>") } buffer.WriteString("</ul>") - return buffer.String() + return template.HTML(buffer.String()) } diff --git a/modules/avatar/hash.go b/modules/avatar/hash.go index 4fc28a7739..50db9c1943 100644 --- a/modules/avatar/hash.go +++ b/modules/avatar/hash.go @@ -4,10 +4,9 @@ package avatar import ( + "crypto/sha256" "encoding/hex" "strconv" - - "github.com/minio/sha256-simd" ) // HashAvatar will generate a unique string, which ensures that when there's a diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go index 9b7a2faf05..63926d5f19 100644 --- a/modules/avatar/identicon/identicon.go +++ b/modules/avatar/identicon/identicon.go @@ -7,11 +7,10 @@ package identicon import ( + "crypto/sha256" "fmt" "image" "image/color" - - "github.com/minio/sha256-simd" ) const minImageSize = 16 diff --git a/modules/badge/badge.go b/modules/badge/badge.go new file mode 100644 index 0000000000..b30d0b4729 --- /dev/null +++ b/modules/badge/badge.go @@ -0,0 +1,104 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package badge + +import ( + actions_model "code.gitea.io/gitea/models/actions" +) + +// The Badge layout: |offset|label|message| +// We use 10x scale to calculate more precisely +// Then scale down to normal size in tmpl file + +type Label struct { + text string + width int +} + +func (l Label) Text() string { + return l.text +} + +func (l Label) Width() int { + return l.width +} + +func (l Label) TextLength() int { + return int(float64(l.width-defaultOffset) * 9.5) +} + +func (l Label) X() int { + return l.width*5 + 10 +} + +type Message struct { + text string + width int + x int +} + +func (m Message) Text() string { + return m.text +} + +func (m Message) Width() int { + return m.width +} + +func (m Message) X() int { + return m.x +} + +func (m Message) TextLength() int { + return int(float64(m.width-defaultOffset) * 9.5) +} + +type Badge struct { + Color string + FontSize int + Label Label + Message Message +} + +func (b Badge) Width() int { + return b.Label.width + b.Message.width +} + +const ( + defaultOffset = 9 + defaultFontSize = 11 + DefaultColor = "#9f9f9f" // Grey + defaultFontWidth = 7 // approximate speculation +) + +var StatusColorMap = map[actions_model.Status]string{ + actions_model.StatusSuccess: "#4c1", // Green + actions_model.StatusSkipped: "#dfb317", // Yellow + actions_model.StatusUnknown: "#97ca00", // Light Green + actions_model.StatusFailure: "#e05d44", // Red + actions_model.StatusCancelled: "#fe7d37", // Orange + actions_model.StatusWaiting: "#dfb317", // Yellow + actions_model.StatusRunning: "#dfb317", // Yellow + actions_model.StatusBlocked: "#dfb317", // Yellow +} + +// GenerateBadge generates badge with given template +func GenerateBadge(label, message, color string) Badge { + lw := defaultFontWidth*len(label) + defaultOffset + mw := defaultFontWidth*len(message) + defaultOffset + x := lw*10 + mw*5 - 10 + return Badge{ + Label: Label{ + text: label, + width: lw, + }, + Message: Message{ + text: message, + width: mw, + x: x, + }, + FontSize: defaultFontSize * 10, + Color: color, + } +} diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go index e920177f89..0f90ec70ce 100644 --- a/modules/base/natural_sort.go +++ b/modules/base/natural_sort.go @@ -4,85 +4,12 @@ package base import ( - "math/big" - "unicode/utf8" + "golang.org/x/text/collate" + "golang.org/x/text/language" ) // NaturalSortLess compares two strings so that they could be sorted in natural order func NaturalSortLess(s1, s2 string) bool { - var i1, i2 int - for { - rune1, j1, end1 := getNextRune(s1, i1) - rune2, j2, end2 := getNextRune(s2, i2) - if end1 || end2 { - return end1 != end2 && end1 - } - dec1 := isDecimal(rune1) - dec2 := isDecimal(rune2) - var less, equal bool - if dec1 && dec2 { - i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2) - } else if !dec1 && !dec2 { - equal = rune1 == rune2 - less = rune1 < rune2 - i1 = j1 - i2 = j2 - } else { - return rune1 < rune2 - } - if !equal { - return less - } - } -} - -func getNextRune(str string, pos int) (rune, int, bool) { - if pos < len(str) { - r, w := utf8.DecodeRuneInString(str[pos:]) - // Fallback to ascii - if r == utf8.RuneError { - r = rune(str[pos]) - w = 1 - } - return r, pos + w, false - } - return 0, pos, true -} - -func isDecimal(r rune) bool { - return '0' <= r && r <= '9' -} - -func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) { - d1, d2 := true, true - var dec1, dec2 string - for d1 || d2 { - if d1 { - r, j, end := getNextRune(str1, pos1) - if !end && isDecimal(r) { - dec1 += string(r) - pos1 = j - } else { - d1 = false - } - } - if d2 { - r, j, end := getNextRune(str2, pos2) - if !end && isDecimal(r) { - dec2 += string(r) - pos2 = j - } else { - d2 = false - } - } - } - less, equal = compareBigNumbers(dec1, dec2) - return pos1, pos2, less, equal -} - -func compareBigNumbers(dec1, dec2 string) (less, equal bool) { - d1, _ := big.NewInt(0).SetString(dec1, 10) - d2, _ := big.NewInt(0).SetString(dec2, 10) - cmp := d1.Cmp(d2) - return cmp < 0, cmp == 0 + c := collate.New(language.English, collate.Numeric) + return c.CompareString(s1, s2) < 0 } diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go index 91e864ad2a..f27a4eb53a 100644 --- a/modules/base/natural_sort_test.go +++ b/modules/base/natural_sort_test.go @@ -11,7 +11,7 @@ import ( func TestNaturalSortLess(t *testing.T) { test := func(s1, s2 string, less bool) { - assert.Equal(t, less, NaturalSortLess(s1, s2)) + assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2) } test("v1.20.0", "v1.2.0", false) test("v1.20.0", "v1.29.0", true) @@ -20,4 +20,11 @@ func TestNaturalSortLess(t *testing.T) { test("a-1-a", "a-1-b", true) test("2", "12", true) test("a", "ab", true) + + test("A", "b", true) + test("a", "B", true) + + test("cafe", "café", true) + test("café", "cafe", false) + test("caff", "café", false) } diff --git a/modules/base/tool.go b/modules/base/tool.go index e9f4dfa279..40785e74e8 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -5,6 +5,7 @@ package base import ( "crypto/sha1" + "crypto/sha256" "encoding/base64" "encoding/hex" "errors" @@ -22,7 +23,6 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/dustin/go-humanize" - "github.com/minio/sha256-simd" ) // EncodeSha1 string to sha1 hex value. @@ -115,7 +115,7 @@ func CreateTimeLimitCode(data string, minutes int, startInf any) string { // create sha1 encode string sh := sha1.New() - _, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, setting.SecretKey, startStr, endStr, minutes))) + _, _ = sh.Write([]byte(fmt.Sprintf("%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, endStr, minutes))) encoded := hex.EncodeToString(sh.Sum(nil)) code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded) @@ -150,13 +150,16 @@ func TruncateString(str string, limit int) string { // StringsToInt64s converts a slice of string to a slice of int64. func StringsToInt64s(strs []string) ([]int64, error) { - ints := make([]int64, len(strs)) - for i := range strs { - n, err := strconv.ParseInt(strs[i], 10, 64) + if strs == nil { + return nil, nil + } + ints := make([]int64, 0, len(strs)) + for _, s := range strs { + n, err := strconv.ParseInt(s, 10, 64) if err != nil { - return ints, err + return nil, err } - ints[i] = n + ints = append(ints, n) } return ints, nil } diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index d28deb593d..f21b89c74c 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -138,12 +138,13 @@ func TestStringsToInt64s(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, result) } + testSuccess(nil, nil) testSuccess([]string{}, []int64{}) testSuccess([]string{"-1234"}, []int64{-1234}) - testSuccess([]string{"1", "4", "16", "64", "256"}, - []int64{1, 4, 16, 64, 256}) + testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256}) - _, err := StringsToInt64s([]string{"-1", "a", "$"}) + ints, err := StringsToInt64s([]string{"-1", "a"}) + assert.Len(t, ints, 0) assert.Error(t, err) } diff --git a/modules/charset/escape_stream.go b/modules/charset/escape_stream.go index 3f08fd94a4..29943eb858 100644 --- a/modules/charset/escape_stream.go +++ b/modules/charset/escape_stream.go @@ -173,7 +173,7 @@ func (e *escapeStreamer) ambiguousRune(r, c rune) error { Val: "ambiguous-code-point", }, html.Attribute{ Key: "data-tooltip-content", - Val: e.locale.Tr("repo.ambiguous_character", r, c), + Val: e.locale.TrString("repo.ambiguous_character", r, c), }); err != nil { return err } diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index a353ced631..9d796a0c18 100644 --- a/modules/charset/escape_test.go +++ b/modules/charset/escape_test.go @@ -4,6 +4,7 @@ package charset import ( + "regexp" "strings" "testing" @@ -156,13 +157,16 @@ func TestEscapeControlReader(t *testing.T) { tests = append(tests, test) } + re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output := &strings.Builder{} status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{}) assert.NoError(t, err) assert.Equal(t, tt.status, *status) - assert.Equal(t, tt.result, output.String()) + outStr := output.String() + outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character") + assert.Equal(t, tt.result, outStr) }) } } diff --git a/modules/container/filter.go b/modules/container/filter.go new file mode 100644 index 0000000000..37ec7c3d56 --- /dev/null +++ b/modules/container/filter.go @@ -0,0 +1,21 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +import "slices" + +// FilterSlice ranges over the slice and calls include() for each element. +// If the second returned value is true, the first returned value will be included in the resulting +// slice (after deduplication). +func FilterSlice[E any, T comparable](s []E, include func(E) (T, bool)) []T { + filtered := make([]T, 0, len(s)) // slice will be clipped before returning + seen := make(map[T]bool, len(s)) + for i := range s { + if v, ok := include(s[i]); ok && !seen[v] { + filtered = append(filtered, v) + seen[v] = true + } + } + return slices.Clip(filtered) +} diff --git a/modules/container/filter_test.go b/modules/container/filter_test.go new file mode 100644 index 0000000000..ad304e5abb --- /dev/null +++ b/modules/container/filter_test.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFilterMapUnique(t *testing.T) { + result := FilterSlice([]int{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + }, func(i int) (int, bool) { + switch i { + case 0: + return 0, true // included later + case 1: + return 0, true // duplicate of previous (should be ignored) + case 2: + return 2, false // not included + default: + return i, true + } + }) + assert.Equal(t, []int{0, 3, 4, 5, 6, 7, 8, 9}, result) +} diff --git a/modules/context/context_test.go b/modules/context/context_test.go deleted file mode 100644 index 033ce2ef0a..0000000000 --- a/modules/context/context_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023 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 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")) -} diff --git a/modules/csv/csv.go b/modules/csv/csv.go index c5497befe7..35c5d6ab67 100644 --- a/modules/csv/csv.go +++ b/modules/csv/csv.go @@ -123,9 +123,9 @@ func guessDelimiter(data []byte) rune { func FormatError(err error, locale translation.Locale) (string, error) { if perr, ok := err.(*stdcsv.ParseError); ok { if perr.Err == stdcsv.ErrFieldCount { - return locale.Tr("repo.error.csv.invalid_field_count", perr.Line), nil + return locale.TrString("repo.error.csv.invalid_field_count", perr.Line), nil } - return locale.Tr("repo.error.csv.unexpected", perr.Line, perr.Column), nil + return locale.TrString("repo.error.csv.unexpected", perr.Line, perr.Column), nil } return "", err diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go index f6e782a5a4..3ddb47acbb 100644 --- a/modules/csv/csv_test.go +++ b/modules/csv/csv_test.go @@ -561,14 +561,14 @@ func TestFormatError(t *testing.T) { err: &csv.ParseError{ Err: csv.ErrFieldCount, }, - expectedMessage: "repo.error.csv.invalid_field_count", + expectedMessage: "repo.error.csv.invalid_field_count:0", expectsError: false, }, { err: &csv.ParseError{ Err: csv.ErrBareQuote, }, - expectedMessage: "repo.error.csv.unexpected", + expectedMessage: "repo.error.csv.unexpected:0,0", expectsError: false, }, { diff --git a/modules/dump/dumper.go b/modules/dump/dumper.go new file mode 100644 index 0000000000..47730851fb --- /dev/null +++ b/modules/dump/dumper.go @@ -0,0 +1,174 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package dump + +import ( + "fmt" + "io" + "os" + "path" + "path/filepath" + "slices" + "strings" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/mholt/archiver/v3" +) + +var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"} + +// PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType" +func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) { + if argFile == "" && argType == "" { + outType = SupportedOutputTypes[0] + outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType) + } else if argFile == "" { + outType = argType + outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType) + } else if argType == "" { + if filepath.Ext(outFileName) == "" { + outType = SupportedOutputTypes[0] + outFileName = argFile + } else { + for _, t := range SupportedOutputTypes { + if strings.HasSuffix(argFile, "."+t) { + outFileName = argFile + outType = t + } + } + } + } else { + outFileName, outType = argFile, argType + } + if !slices.Contains(SupportedOutputTypes, outType) { + return "", "" + } + return outFileName, outType +} + +func IsSubdir(upper, lower string) (bool, error) { + if relPath, err := filepath.Rel(upper, lower); err != nil { + return false, err + } else if relPath == "." || !strings.HasPrefix(relPath, ".") { + return true, nil + } + return false, nil +} + +type Dumper struct { + Writer archiver.Writer + Verbose bool + + globalExcludeAbsPaths []string +} + +func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error { + if dumper.Verbose { + log.Info("Adding file %s", customName) + } + + return dumper.Writer.Write(archiver.File{ + FileInfo: archiver.FileInfo{ + FileInfo: info, + CustomName: customName, + }, + ReadCloser: r, + }) +} + +func (dumper *Dumper) AddFile(filePath, absPath string) error { + file, err := os.Open(absPath) + if err != nil { + return err + } + defer file.Close() + fileInfo, err := file.Stat() + if err != nil { + return err + } + return dumper.AddReader(file, fileInfo, filePath) +} + +func (dumper *Dumper) normalizeFilePath(absPath string) string { + absPath = filepath.Clean(absPath) + if setting.IsWindows { + absPath = strings.ToLower(absPath) + } + return absPath +} + +func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) { + for _, absPath := range absPaths { + dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath)) + } +} + +func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool { + norm := dumper.normalizeFilePath(absPath) + return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm) +} + +func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error { + excludes = slices.Clone(excludes) + for i := range excludes { + excludes[i] = dumper.normalizeFilePath(excludes[i]) + } + return dumper.addFileOrDir(insidePath, absPath, excludes) +} + +func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error { + absPath, err := filepath.Abs(absPath) + if err != nil { + return err + } + dir, err := os.Open(absPath) + if err != nil { + return err + } + defer dir.Close() + + files, err := dir.Readdir(0) + if err != nil { + return err + } + for _, file := range files { + currentAbsPath := filepath.Join(absPath, file.Name()) + if dumper.shouldExclude(currentAbsPath, excludes) { + continue + } + + currentInsidePath := path.Join(insidePath, file.Name()) + if file.IsDir() { + if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil { + return err + } + if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil { + return err + } + } else { + // only copy regular files and symlink regular files, skip non-regular files like socket/pipe/... + shouldAdd := file.Mode().IsRegular() + if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink { + target, err := filepath.EvalSymlinks(currentAbsPath) + if err != nil { + return err + } + targetStat, err := os.Stat(target) + if err != nil { + return err + } + shouldAdd = targetStat.Mode().IsRegular() + } + if shouldAdd { + if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil { + return err + } + } + } + } + return nil +} diff --git a/modules/dump/dumper_test.go b/modules/dump/dumper_test.go new file mode 100644 index 0000000000..b444fa2de5 --- /dev/null +++ b/modules/dump/dumper_test.go @@ -0,0 +1,113 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package dump + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + "testing" + "time" + + "code.gitea.io/gitea/modules/timeutil" + + "github.com/mholt/archiver/v3" + "github.com/stretchr/testify/assert" +) + +func TestPrepareFileNameAndType(t *testing.T) { + defer timeutil.MockSet(time.Unix(1234, 0))() + test := func(argFile, argType, expFile, expType string) { + outFile, outType := PrepareFileNameAndType(argFile, argType) + assert.Equal(t, + fmt.Sprintf("outFile=%s, outType=%s", expFile, expType), + fmt.Sprintf("outFile=%s, outType=%s", outFile, outType), + fmt.Sprintf("argFile=%s, argType=%s", argFile, argType), + ) + } + + test("", "", "gitea-dump-1234.zip", "zip") + test("", "tar.gz", "gitea-dump-1234.tar.gz", "tar.gz") + test("", "no-such", "", "") + + test("-", "", "-", "zip") + test("-", "tar.gz", "-", "tar.gz") + test("-", "no-such", "", "") + + test("a", "", "a", "zip") + test("a", "tar.gz", "a", "tar.gz") + test("a", "no-such", "", "") + + test("a.zip", "", "a.zip", "zip") + test("a.zip", "tar.gz", "a.zip", "tar.gz") + test("a.zip", "no-such", "", "") + + test("a.tar.gz", "", "a.tar.gz", "zip") + test("a.tar.gz", "tar.gz", "a.tar.gz", "tar.gz") + test("a.tar.gz", "no-such", "", "") +} + +func TestIsSubDir(t *testing.T) { + tmpDir := t.TempDir() + _ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755) + + isSub, err := IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include")) + assert.NoError(t, err) + assert.True(t, isSub) + + isSub, err = IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include/sub")) + assert.NoError(t, err) + assert.True(t, isSub) + + isSub, err = IsSubdir(filepath.Join(tmpDir, "include/sub"), filepath.Join(tmpDir, "include")) + assert.NoError(t, err) + assert.False(t, isSub) +} + +type testWriter struct { + added []string +} + +func (t *testWriter) Create(out io.Writer) error { + return nil +} + +func (t *testWriter) Write(f archiver.File) error { + t.added = append(t.added, f.Name()) + return nil +} + +func (t *testWriter) Close() error { + return nil +} + +func TestDumper(t *testing.T) { + sortStrings := func(s []string) []string { + sort.Strings(s) + return s + } + tmpDir := t.TempDir() + _ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude1"), 0o755) + _ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude2"), 0o755) + _ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755) + _ = os.WriteFile(filepath.Join(tmpDir, "include/a"), nil, 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "include/sub/b"), nil, 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "include/exclude1/a-1"), nil, 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "include/exclude2/a-2"), nil, 0o644) + + tw := &testWriter{} + d := &Dumper{Writer: tw} + d.GlobalExcludeAbsPath(filepath.Join(tmpDir, "include/exclude1")) + err := d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), []string{filepath.Join(tmpDir, "include/exclude2")}) + assert.NoError(t, err) + assert.EqualValues(t, sortStrings([]string{"include/a", "include/sub", "include/sub/b"}), sortStrings(tw.added)) + + tw = &testWriter{} + d = &Dumper{Writer: tw} + err = d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), nil) + assert.NoError(t, err) + assert.EqualValues(t, sortStrings([]string{"include/exclude2", "include/exclude2/a-2", "include/a", "include/sub", "include/sub/b", "include/exclude1", "include/exclude1/a-1"}), sortStrings(tw.added)) +} diff --git a/modules/generate/generate.go b/modules/generate/generate.go index ee3c76059b..2d9a3dd902 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -7,6 +7,7 @@ package generate import ( "crypto/rand" "encoding/base64" + "fmt" "io" "time" @@ -38,19 +39,24 @@ func NewInternalToken() (string, error) { return internalToken, nil } -// NewJwtSecret generates a new value intended to be used for JWT secrets. -func NewJwtSecret() ([]byte, error) { - bytes := make([]byte, 32) - _, err := io.ReadFull(rand.Reader, bytes) - if err != nil { +const defaultJwtSecretLen = 32 + +// DecodeJwtSecretBase64 decodes a base64 encoded jwt secret into bytes, and check its length +func DecodeJwtSecretBase64(src string) ([]byte, error) { + encoding := base64.RawURLEncoding + decoded := make([]byte, encoding.DecodedLen(len(src))+3) + if n, err := encoding.Decode(decoded, []byte(src)); err != nil { return nil, err + } else if n != defaultJwtSecretLen { + return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen) } - return bytes, nil + return decoded[:defaultJwtSecretLen], nil } -// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets. -func NewJwtSecretBase64() ([]byte, string, error) { - bytes, err := NewJwtSecret() +// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file +func NewJwtSecretWithBase64() ([]byte, string, error) { + bytes := make([]byte, defaultJwtSecretLen) + _, err := io.ReadFull(rand.Reader, bytes) if err != nil { return nil, "", err } diff --git a/modules/generate/generate_test.go b/modules/generate/generate_test.go new file mode 100644 index 0000000000..af640a60c1 --- /dev/null +++ b/modules/generate/generate_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package generate + +import ( + "encoding/base64" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeJwtSecretBase64(t *testing.T) { + _, err := DecodeJwtSecretBase64("abcd") + assert.ErrorContains(t, err, "invalid base64 decoded length") + _, err = DecodeJwtSecretBase64(strings.Repeat("a", 64)) + assert.ErrorContains(t, err, "invalid base64 decoded length") + + str32 := strings.Repeat("x", 32) + encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32)) + decoded32, err := DecodeJwtSecretBase64(encoded32) + assert.NoError(t, err) + assert.Equal(t, str32, string(decoded32)) +} + +func TestNewJwtSecretWithBase64(t *testing.T) { + secret, encoded, err := NewJwtSecretWithBase64() + assert.NoError(t, err) + assert.Len(t, secret, 32) + decoded, err := DecodeJwtSecretBase64(encoded) + assert.NoError(t, err) + assert.Equal(t, secret, decoded) +} diff --git a/modules/git/attribute.go b/modules/git/attribute.go new file mode 100644 index 0000000000..4dfa510369 --- /dev/null +++ b/modules/git/attribute.go @@ -0,0 +1,35 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "code.gitea.io/gitea/modules/optional" +) + +const ( + AttributeLinguistVendored = "linguist-vendored" + AttributeLinguistGenerated = "linguist-generated" + AttributeLinguistDocumentation = "linguist-documentation" + AttributeLinguistDetectable = "linguist-detectable" + AttributeLinguistLanguage = "linguist-language" + AttributeGitlabLanguage = "gitlab-language" +) + +// true if "set"/"true", false if "unset"/"false", none otherwise +func AttributeToBool(attr map[string]string, name string) optional.Option[bool] { + switch attr[name] { + case "set", "true": + return optional.Some(true) + case "unset", "false": + return optional.Some(false) + } + return optional.None[bool]() +} + +func AttributeToString(attr map[string]string, name string) optional.Option[string] { + if value, has := attr[name]; has && value != "unspecified" { + return optional.Some(value) + } + return optional.None[string]() +} diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 53a9393d5f..043dbb44bd 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -203,16 +203,7 @@ headerLoop: } // Discard the rest of the tag - discard := size - n + 1 - for discard > math.MaxInt32 { - _, err := rd.Discard(math.MaxInt32) - if err != nil { - return id, err - } - discard -= math.MaxInt32 - } - _, err := rd.Discard(int(discard)) - return id, err + return id, DiscardFull(rd, size-n+1) } // ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream. @@ -238,16 +229,7 @@ headerLoop: } // Discard the rest of the commit - discard := size - n + 1 - for discard > math.MaxInt32 { - _, err := rd.Discard(math.MaxInt32) - if err != nil { - return id, err - } - discard -= math.MaxInt32 - } - _, err := rd.Discard(int(discard)) - return id, err + return id, DiscardFull(rd, size-n+1) } // git tree files are a list: @@ -345,3 +327,21 @@ func init() { _, filename, _, _ := runtime.Caller(0) callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go") } + +func DiscardFull(rd *bufio.Reader, discard int64) error { + if discard > math.MaxInt32 { + n, err := rd.Discard(math.MaxInt32) + discard -= int64(n) + if err != nil { + return err + } + } + for discard > 0 { + n, err := rd.Discard(int(discard)) + discard -= int64(n) + if err != nil { + return err + } + } + return nil +} diff --git a/modules/git/blame.go b/modules/git/blame.go index 64095a218a..69e1b08f93 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -115,6 +115,10 @@ func (r *BlameReader) NextPart() (*BlamePart, error) { // Close BlameReader - don't run NextPart after invoking that func (r *BlameReader) Close() error { + if r.bufferedReader == nil { + return nil + } + err := <-r.done r.bufferedReader = nil _ = r.reader.Close() diff --git a/modules/git/blame_sha256_test.go b/modules/git/blame_sha256_test.go index 01de0454a3..8cd345714f 100644 --- a/modules/git/blame_sha256_test.go +++ b/modules/git/blame_sha256_test.go @@ -118,11 +118,12 @@ func TestReadingBlameOutputSha256(t *testing.T) { }, } + objectFormat, err := repo.GetObjectFormat() + assert.NoError(t, err) for _, c := range cases { commit, err := repo.GetCommit(c.CommitID) assert.NoError(t, err) - - blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass) + blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() diff --git a/modules/git/blame_test.go b/modules/git/blame_test.go index 327edab767..4220c85600 100644 --- a/modules/git/blame_test.go +++ b/modules/git/blame_test.go @@ -118,11 +118,13 @@ func TestReadingBlameOutput(t *testing.T) { }, } + objectFormat, err := repo.GetObjectFormat() + assert.NoError(t, err) for _, c := range cases { commit, err := repo.GetCommit(c.CommitID) assert.NoError(t, err) - blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass) + blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go index 6e8a48b1db..945a6bc432 100644 --- a/modules/git/blob_nogogit.go +++ b/modules/git/blob_nogogit.go @@ -9,7 +9,6 @@ import ( "bufio" "bytes" "io" - "math" "code.gitea.io/gitea/modules/log" ) @@ -103,26 +102,17 @@ func (b *blobReader) Read(p []byte) (n int, err error) { // Close implements io.Closer func (b *blobReader) Close() error { - defer b.cancel() - if b.n > 0 { - for b.n > math.MaxInt32 { - n, err := b.rd.Discard(math.MaxInt32) - b.n -= int64(n) - if err != nil { - return err - } - b.n -= math.MaxInt32 - } - n, err := b.rd.Discard(int(b.n)) - b.n -= int64(n) - if err != nil { - return err - } + if b.rd == nil { + return nil } - if b.n == 0 { - _, err := b.rd.Discard(1) - b.n-- + + defer b.cancel() + + if err := DiscardFull(b.rd, b.n+1); err != nil { return err } + + b.rd = nil + return nil } diff --git a/modules/git/command.go b/modules/git/command.go index 9305ef6f92..22cb275ab2 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -12,6 +12,7 @@ import ( "io" "os" "os/exec" + "runtime" "strings" "time" @@ -344,6 +345,17 @@ func (c *Command) Run(opts *RunOpts) error { log.Debug("slow git.Command.Run: %s (%s)", c, elapsed) } + // We need to check if the context is canceled by the program on Windows. + // This is because Windows does not have signal checking when terminating the process. + // It always returns exit code 1, unlike Linux, which has many exit codes for signals. + if runtime.GOOS == "windows" && + err != nil && + err.Error() == "" && + cmd.ProcessState.ExitCode() == 1 && + ctx.Err() == context.Canceled { + return ctx.Err() + } + if err != nil && ctx.Err() != context.DeadlineExceeded { return err } @@ -355,7 +367,6 @@ type RunStdError interface { error Unwrap() error Stderr() string - IsExitCode(code int) bool } type runStdError struct { @@ -380,9 +391,9 @@ func (r *runStdError) Stderr() string { return r.stderr } -func (r *runStdError) IsExitCode(code int) bool { +func IsErrorExitCode(err error, code int) bool { var exitError *exec.ExitError - if errors.As(r.err, &exitError) { + if errors.As(err, &exitError) { return exitError.ExitCode() == code } return false diff --git a/modules/git/commit.go b/modules/git/commit.go index 5d960e92f3..5f442b0e1a 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "errors" + "fmt" "io" "os/exec" "strconv" @@ -25,14 +26,14 @@ type Commit struct { Author *Signature Committer *Signature CommitMessage string - Signature *CommitGPGSignature + Signature *CommitSignature Parents []ObjectID // ID strings submoduleCache *ObjectCache } -// CommitGPGSignature represents a git commit signature part. -type CommitGPGSignature struct { +// CommitSignature represents a git commit signature part. +type CommitSignature struct { Signature string Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data } @@ -311,7 +312,7 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String()) } -// FileChangedSinceCommit Returns true if the file given has changed since the the past commit +// FileChangedSinceCommit Returns true if the file given has changed since the past commit // YOU MUST ENSURE THAT pastCommit is a valid commit ID. func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) { return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String()) @@ -396,6 +397,9 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) { } } } + if err = scanner.Err(); err != nil { + return nil, fmt.Errorf("GetSubModules scan: %w", err) + } return c.submoduleCache, nil } diff --git a/modules/git/commit_convert_gogit.go b/modules/git/commit_convert_gogit.go index 819ea0d1db..d7b945ed6b 100644 --- a/modules/git/commit_convert_gogit.go +++ b/modules/git/commit_convert_gogit.go @@ -13,7 +13,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -func convertPGPSignature(c *object.Commit) *CommitGPGSignature { +func convertPGPSignature(c *object.Commit) *CommitSignature { if c.PGPSignature == "" { return nil } @@ -47,11 +47,17 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature { return nil } + if c.Encoding != "" && c.Encoding != "UTF-8" { + if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil { + return nil + } + } + if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil { return nil } - return &CommitGPGSignature{ + return &CommitSignature{ Signature: c.PGPSignature, Payload: w.String(), } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index e469d2cab6..a5d18694f7 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -151,6 +151,9 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, return nil, err } if typ != "commit" { + if err := DiscardFull(batchReader, size+1); err != nil { + return nil, err + } return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID) } c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size)) diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go index 4809d6c7ed..f1f4a0e588 100644 --- a/modules/git/commit_reader.go +++ b/modules/git/commit_reader.go @@ -84,6 +84,8 @@ readLoop: commit.Committer = &Signature{} commit.Committer.Decode(data) _, _ = payloadSB.Write(line) + case "encoding": + _, _ = payloadSB.Write(line) case "gpgsig": fallthrough case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present. @@ -97,7 +99,7 @@ readLoop: } } commit.CommitMessage = messageSB.String() - commit.Signature = &CommitGPGSignature{ + commit.Signature = &CommitSignature{ Signature: signatureSB.String(), Payload: payloadSB.String(), } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 82112cb409..3b8b6d3763 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -140,10 +140,13 @@ func TestHasPreviousCommitSha256(t *testing.T) { commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc") assert.NoError(t, err) + objectFormat, err := repo.GetObjectFormat() + assert.NoError(t, err) + parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236") - assert.Equal(t, repo.objectFormat, parentSHA.Type()) - assert.Equal(t, repo.objectFormat.Name(), "sha256") + assert.Equal(t, objectFormat, parentSHA.Type()) + assert.Equal(t, objectFormat.Name(), "sha256") haz, err := commit.HasPreviousCommit(parentSHA) assert.NoError(t, err) diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index e512eecc56..a33e7df31a 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -125,6 +125,73 @@ empty commit`, commitFromReader.Signature.Payload) assert.EqualValues(t, commitFromReader, commitFromReader2) } +func TestCommitWithEncodingFromReader(t *testing.T) { + commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074 +tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 +parent 47b24e7ab977ed31c5a39989d570847d6d0052af +author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 +committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 +encoding ISO-8859-1 +gpgsig -----BEGIN PGP SIGNATURE----- + + iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow + Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR + gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq + zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr + frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j + FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd + G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn + SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F + yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz + jw4YcO5u + =r3UU + -----END PGP SIGNATURE----- + +ISO-8859-1` + + sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2} + gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) + assert.NoError(t, err) + assert.NotNil(t, gitRepo) + defer gitRepo.Close() + + commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) + assert.NoError(t, err) + if !assert.NotNil(t, commitFromReader) { + return + } + assert.EqualValues(t, sha, commitFromReader.ID) + assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- + +iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow +Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR +gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq +zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr +frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j +FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd +G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn +SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F +yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz +jw4YcO5u +=r3UU +-----END PGP SIGNATURE----- +`, commitFromReader.Signature.Signature) + assert.EqualValues(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 +parent 47b24e7ab977ed31c5a39989d570847d6d0052af +author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 +committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 +encoding ISO-8859-1 + +ISO-8859-1`, commitFromReader.Signature.Payload) + assert.EqualValues(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String()) + + commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) + assert.NoError(t, err) + commitFromReader.CommitMessage += "\n\n" + commitFromReader.Signature.Payload += "\n\n" + assert.EqualValues(t, commitFromReader, commitFromReader2) +} + func TestHasPreviousCommit(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") diff --git a/modules/git/error.go b/modules/git/error.go index dc10d451b3..91d25eca69 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -96,7 +96,7 @@ func (err ErrBranchNotExist) Unwrap() error { return util.ErrNotExist } -// ErrPushOutOfDate represents an error if merging fails due to unrelated histories +// ErrPushOutOfDate represents an error if merging fails due to the base branch being updated type ErrPushOutOfDate struct { StdOut string StdErr string diff --git a/modules/git/git.go b/modules/git/git.go index 2a1ccab499..6afacaf117 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -33,42 +33,45 @@ var ( // DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx DefaultContext context.Context - SupportProcReceive bool // >= 2.29 - SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ + DefaultFeatures struct { + GitVersion *version.Version - gitVersion *version.Version + SupportProcReceive bool // >= 2.29 + SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ + } ) -// loadGitVersion returns current Git version from shell. Internal usage only. -func loadGitVersion() (*version.Version, error) { +// loadGitVersion tries to get the current git version and stores it into a global variable +func loadGitVersion() error { // doesn't need RWMutex because it's executed by Init() - if gitVersion != nil { - return gitVersion, nil + if DefaultFeatures.GitVersion != nil { + return nil } stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil) if runErr != nil { - return nil, runErr + return runErr } - fields := strings.Fields(stdout) + ver, err := parseGitVersionLine(strings.TrimSpace(stdout)) + if err == nil { + DefaultFeatures.GitVersion = ver + } + return err +} + +func parseGitVersionLine(s string) (*version.Version, error) { + fields := strings.Fields(s) if len(fields) < 3 { - return nil, fmt.Errorf("invalid git version output: %s", stdout) + return nil, fmt.Errorf("invalid git version: %q", s) } - var versionString string - - // Handle special case on Windows. - i := strings.Index(fields[2], "windows") - if i >= 1 { - versionString = fields[2][:i-1] - } else { - versionString = fields[2] + // version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1" + versionString := fields[2] + if pos := strings.Index(versionString, "windows"); pos >= 1 { + versionString = versionString[:pos-1] } - - var err error - gitVersion, err = version.NewVersion(versionString) - return gitVersion, err + return version.NewVersion(versionString) } // SetExecutablePath changes the path of git executable and checks the file permission and version. @@ -83,8 +86,7 @@ func SetExecutablePath(path string) error { } GitExecutable = absPath - _, err = loadGitVersion() - if err != nil { + if err = loadGitVersion(); err != nil { return fmt.Errorf("unable to load git version: %w", err) } @@ -93,7 +95,7 @@ func SetExecutablePath(path string) error { return err } - if gitVersion.LessThan(versionRequired) { + if DefaultFeatures.GitVersion.LessThan(versionRequired) { moreHint := "get git: https://git-scm.com/download/" if runtime.GOOS == "linux" { // there are a lot of CentOS/RHEL users using old git, so we add a special hint for them @@ -102,19 +104,22 @@ func SetExecutablePath(path string) error { moreHint = "get git: https://git-scm.com/download/linux and https://ius.io" } } - return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint) + return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures.GitVersion.Original(), RequiredVersion, moreHint) } + if err = checkGitVersionCompatibility(DefaultFeatures.GitVersion); err != nil { + return fmt.Errorf("installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git", DefaultFeatures.GitVersion.String(), err) + } return nil } // VersionInfo returns git version information func VersionInfo() string { - if gitVersion == nil { + if DefaultFeatures.GitVersion == nil { return "(git not found)" } format := "%s" - args := []any{gitVersion.Original()} + args := []any{DefaultFeatures.GitVersion.Original()} // Since git wire protocol has been released from git v2.18 if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { format += ", Wire Protocol %s Enabled" @@ -184,9 +189,9 @@ func InitFull(ctx context.Context) (err error) { if CheckGitVersionAtLeast("2.9") == nil { globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") } - SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil - SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit - if SupportHashSha256 { + DefaultFeatures.SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil + DefaultFeatures.SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit + if DefaultFeatures.SupportHashSha256 { SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat) } else { log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported") @@ -251,7 +256,7 @@ func syncGitConfig() (err error) { } } - if SupportProcReceive { + if DefaultFeatures.SupportProcReceive { // set support for AGit flow if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { return err @@ -262,19 +267,18 @@ func syncGitConfig() (err error) { } } - // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user - // however, some docker users and samba users find it difficult to configure their systems so that Gitea's git repositories are owned by the Gitea user. (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.) - // see issue: https://github.com/go-gitea/gitea/issues/19455 - // Fundamentally the problem lies with the uid-gid-mapping mechanism for filesystems in docker on windows (and to a lesser extent samba). - // Docker's configuration mechanism for local filesystems provides no way of setting this mapping and although there is a mechanism for setting this uid through using cifs mounting it is complicated and essentially undocumented - // Thus the owner uid/gid for files on these filesystems will be marked as root. + // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user. + // However, some docker users and samba users find it difficult to configure their systems correctly, + // so that Gitea's git repositories are owned by the Gitea user. + // (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.) + // See issue: https://github.com/go-gitea/gitea/issues/19455 // As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea, // it is now safe to set "safe.directory=*" for internal usage only. - // Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - // Although only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - this setting is tolerated by earlier versions + // Although this setting is only supported by some new git versions, it is also tolerated by earlier versions if err := configAddNonExist("safe.directory", "*"); err != nil { return err } + if runtime.GOOS == "windows" { if err := configSet("core.longpaths", "true"); err != nil { return err @@ -307,22 +311,37 @@ func syncGitConfig() (err error) { // CheckGitVersionAtLeast check git version is at least the constraint version func CheckGitVersionAtLeast(atLeast string) error { - if _, err := loadGitVersion(); err != nil { - return err + if DefaultFeatures.GitVersion == nil { + panic("git module is not initialized") // it shouldn't happen } atLeastVersion, err := version.NewVersion(atLeast) if err != nil { return err } - if gitVersion.Compare(atLeastVersion) < 0 { - return fmt.Errorf("installed git binary version %s is not at least %s", gitVersion.Original(), atLeast) + if DefaultFeatures.GitVersion.Compare(atLeastVersion) < 0 { + return fmt.Errorf("installed git binary version %s is not at least %s", DefaultFeatures.GitVersion.Original(), atLeast) + } + return nil +} + +func checkGitVersionCompatibility(gitVer *version.Version) error { + badVersions := []struct { + Version *version.Version + Reason string + }{ + {version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"}, + } + for _, bad := range badVersions { + if gitVer.Equal(bad.Version) { + return errors.New(bad.Reason) + } } return nil } func configSet(key, value string) error { stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) - if err != nil && !err.IsExitCode(1) { + if err != nil && !IsErrorExitCode(err, 1) { return fmt.Errorf("failed to get git config %s, err: %w", key, err) } @@ -345,7 +364,7 @@ func configSetNonExist(key, value string) error { // already exist return nil } - if err.IsExitCode(1) { + if IsErrorExitCode(err, 1) { // not exist, set new config _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) if err != nil { @@ -363,7 +382,7 @@ func configAddNonExist(key, value string) error { // already exist return nil } - if err.IsExitCode(1) { + if IsErrorExitCode(err, 1) { // not exist, add new config _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) if err != nil { @@ -384,7 +403,7 @@ func configUnsetAll(key, value string) error { } return nil } - if err.IsExitCode(1) { + if IsErrorExitCode(err, 1) { // not exist return nil } diff --git a/modules/git/git_test.go b/modules/git/git_test.go index 37ab669ea4..fc92bebe04 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "github.com/hashicorp/go-version" "github.com/stretchr/testify/assert" ) @@ -93,3 +94,25 @@ func TestSyncConfig(t *testing.T) { assert.True(t, gitConfigContains("[sync-test]")) assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) } + +func TestParseGitVersion(t *testing.T) { + v, err := parseGitVersionLine("git version 2.29.3") + assert.NoError(t, err) + assert.Equal(t, "2.29.3", v.String()) + + v, err = parseGitVersionLine("git version 2.29.3.windows.1") + assert.NoError(t, err) + assert.Equal(t, "2.29.3", v.String()) + + _, err = parseGitVersionLine("git version") + assert.Error(t, err) + + _, err = parseGitVersionLine("git version windows") + assert.Error(t, err) +} + +func TestCheckGitVersionCompatibility(t *testing.T) { + assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.0")))) + assert.ErrorContains(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.1"))), "regression bug of GIT_FLUSH") + assert.NoError(t, checkGitVersionCompatibility(version.Must(version.NewVersion("2.43.2")))) +} diff --git a/modules/git/grep.go b/modules/git/grep.go new file mode 100644 index 0000000000..e7d238e586 --- /dev/null +++ b/modules/git/grep.go @@ -0,0 +1,130 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "os" + "slices" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/util" +) + +type GrepResult struct { + Filename string + LineNumbers []int + LineCodes []string +} + +type GrepOptions struct { + RefName string + MaxResultLimit int + ContextLineNumber int + IsFuzzy bool + MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated +} + +func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, fmt.Errorf("unable to create os pipe to grep: %w", err) + } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + + /* + The output is like this ( "^@" means \x00): + + HEAD:.air.toml + 6^@bin = "gitea" + + HEAD:.changelog.yml + 2^@repo: go-gitea/gitea + */ + var results []*GrepResult + cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name") + cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) + if opts.IsFuzzy { + words := strings.Fields(search) + for _, word := range words { + cmd.AddOptionValues("-e", strings.TrimLeft(word, "-")) + } + } else { + cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) + } + cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) + opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) + stderr := bytes.Buffer{} + err = cmd.Run(&RunOpts{ + Dir: repo.Path, + Stdout: stdoutWriter, + Stderr: &stderr, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + defer stdoutReader.Close() + + isInBlock := false + rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024)) + var res *GrepResult + for { + lineBytes, isPrefix, err := rd.ReadLine() + if isPrefix { + lineBytes = slices.Clone(lineBytes) + for isPrefix && err == nil { + _, isPrefix, err = rd.ReadLine() + } + } + if len(lineBytes) == 0 && err != nil { + break + } + line := string(lineBytes) // the memory of lineBytes is mutable + if !isInBlock { + if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok { + isInBlock = true + res = &GrepResult{Filename: filename} + results = append(results, res) + } + continue + } + if line == "" { + if len(results) >= opts.MaxResultLimit { + cancel() + break + } + isInBlock = false + continue + } + if line == "--" { + continue + } + if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok { + lineNumInt, _ := strconv.Atoi(lineNum) + res.LineNumbers = append(res.LineNumbers, lineNumInt) + res.LineCodes = append(res.LineCodes, lineCode) + } + } + return nil + }, + }) + // git grep exits by cancel (killed), usually it is caused by the limit of results + if IsErrorExitCode(err, -1) && stderr.Len() == 0 { + return results, nil + } + // git grep exits with 1 if no results are found + if IsErrorExitCode(err, 1) && stderr.Len() == 0 { + return nil, nil + } + if err != nil && !errors.Is(err, context.Canceled) { + return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String()) + } + return results, nil +} diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go new file mode 100644 index 0000000000..7f4ded478f --- /dev/null +++ b/modules/git/grep_test.go @@ -0,0 +1,61 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGrepSearch(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "language_stats_repo")) + assert.NoError(t, err) + defer repo.Close() + + res, err := GrepSearch(context.Background(), repo, "void", GrepOptions{}) + assert.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "java-hello/main.java", + LineNumbers: []int{3}, + LineCodes: []string{" public static void main(String[] args)"}, + }, + { + Filename: "main.vendor.java", + LineNumbers: []int{3}, + LineCodes: []string{" public static void main(String[] args)"}, + }, + }, res) + + res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1}) + assert.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "java-hello/main.java", + LineNumbers: []int{3}, + LineCodes: []string{" public static void main(String[] args)"}, + }, + }, res) + + res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1, MaxLineLength: 39}) + assert.NoError(t, err) + assert.Equal(t, []*GrepResult{ + { + Filename: "java-hello/main.java", + LineNumbers: []int{3}, + LineCodes: []string{" public static void main(String[] arg"}, + }, + }, res) + + res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) + assert.NoError(t, err) + assert.Len(t, res, 0) + + res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) + assert.Error(t, err) + assert.Len(t, res, 0) +} diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index 7c7baedd2f..5b62b90b27 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -4,12 +4,11 @@ package git import ( + "crypto/sha256" "fmt" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - - "github.com/minio/sha256-simd" ) // Cache represents a caching interface diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 26a0d28098..9e345f3ee0 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -143,19 +143,19 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int } // Our "line" must look like: <commitid> SP (<parent> SP) * NUL - commitIds := string(g.next) + commitIDs := string(g.next) if g.buffull { more, err := g.rd.ReadString('\x00') if err != nil { return nil, err } - commitIds += more + commitIDs += more } - commitIds = commitIds[:len(commitIds)-1] - splitIds := strings.Split(commitIds, " ") - ret.CommitID = splitIds[0] - if len(splitIds) > 1 { - ret.ParentIDs = splitIds[1:] + commitIDs = commitIDs[:len(commitIDs)-1] + splitIDs := strings.Split(commitIDs, " ") + ret.CommitID = splitIDs[0] + if len(splitIDs) > 1 { + ret.ParentIDs = splitIDs[1:] } // now read the next "line" diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go index a725f4799d..4c65249089 100644 --- a/modules/git/pipeline/lfs_nogogit.go +++ b/modules/git/pipeline/lfs_nogogit.go @@ -169,6 +169,10 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err } else { break commitReadingLoop } + default: + if err := git.DiscardFull(batchReader, size+1); err != nil { + return nil, err + } } } } diff --git a/modules/git/repo.go b/modules/git/repo.go index 028ca485f3..c5ba5117a7 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -74,7 +74,7 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma if !IsValidObjectFormat(objectFormatName) { return fmt.Errorf("invalid object format: %s", objectFormatName) } - if SupportHashSha256 { + if DefaultFeatures.SupportHashSha256 { cmd.AddOptionValues("--object-format", objectFormatName) } @@ -244,7 +244,7 @@ func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error return time.Time{}, err } commitTime := strings.TrimSpace(stdout) - return time.Parse(GitTimeLayout, commitTime) + return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) } // DivergeObject represents commit count diverging commits @@ -256,7 +256,7 @@ type DivergeObject struct { // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) { cmd := NewCommand(ctx, "rev-list", "--count", "--left-right"). - AddDynamicArguments(baseBranch + "..." + targetBranch) + AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--") stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) if err != nil { return do, err diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 2b34f117f7..84f85d1b1a 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -291,10 +291,17 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe } checker := &CheckAttributeReader{ - Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, - Repo: repo, - IndexFile: indexFilename, - WorkTree: worktree, + Attributes: []string{ + AttributeLinguistVendored, + AttributeLinguistGenerated, + AttributeLinguistDocumentation, + AttributeLinguistDetectable, + AttributeLinguistLanguage, + AttributeGitlabLanguage, + }, + Repo: repo, + IndexFile: indexFilename, + WorkTree: worktree, } ctx, cancel := context.WithCancel(repo.Ctx) if err := checker.Init(ctx); err != nil { diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go index ed16dccbe4..0fcd94b4c7 100644 --- a/modules/git/repo_attribute_test.go +++ b/modules/git/repo_attribute_test.go @@ -24,7 +24,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { select { case attr := <-wr.ReadAttribute(): assert.Equal(t, ".gitignore\"\n", attr.Filename) - assert.Equal(t, "linguist-vendored", attr.Attribute) + assert.Equal(t, AttributeLinguistVendored, attr.Attribute) assert.Equal(t, "unspecified", attr.Value) case <-time.After(100 * time.Millisecond): assert.FailNow(t, "took too long to read an attribute from the list") @@ -38,7 +38,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { select { case attr := <-wr.ReadAttribute(): assert.Equal(t, ".gitignore\"\n", attr.Filename) - assert.Equal(t, "linguist-vendored", attr.Attribute) + assert.Equal(t, AttributeLinguistVendored, attr.Attribute) assert.Equal(t, "unspecified", attr.Value) case <-time.After(100 * time.Millisecond): assert.FailNow(t, "took too long to read an attribute from the list") @@ -77,21 +77,21 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, attributeTriple{ Filename: "shouldbe.vendor", - Attribute: "linguist-vendored", + Attribute: AttributeLinguistVendored, Value: "set", }, attr) attr = <-wr.ReadAttribute() assert.NoError(t, err) assert.EqualValues(t, attributeTriple{ Filename: "shouldbe.vendor", - Attribute: "linguist-generated", + Attribute: AttributeLinguistGenerated, Value: "unspecified", }, attr) attr = <-wr.ReadAttribute() assert.NoError(t, err) assert.EqualValues(t, attributeTriple{ Filename: "shouldbe.vendor", - Attribute: "linguist-language", + Attribute: AttributeLinguistLanguage, Value: "unspecified", }, attr) } diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index 9270bb70f0..0cd07dcdc8 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -8,11 +8,11 @@ package git import ( "context" - "errors" "path/filepath" gitealog "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" @@ -52,7 +52,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { if err != nil { return nil, err } else if !isDir(repoPath) { - return nil, errors.New("no such file or directory") + return nil, util.NewNotExistErrorf("no such file or directory") } fs := osfs.New(repoPath) @@ -88,16 +88,17 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { } // Close this repository, in particular close the underlying gogitStorage if this is not nil -func (repo *Repository) Close() (err error) { +func (repo *Repository) Close() error { if repo == nil || repo.gogitStorage == nil { - return + return nil } if err := repo.gogitStorage.Close(); err != nil { gitealog.Error("Error closing storage: %v", err) } + repo.gogitStorage = nil repo.LastCommitCache = nil repo.tagCache = nil - return + return nil } // GoGitRepo gets the go-git repo representation diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index d5a350a926..5511526e78 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -9,10 +9,10 @@ package git import ( "bufio" "context" - "errors" "path/filepath" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) func init() { @@ -27,10 +27,12 @@ type Repository struct { gpgSettings *GPGSettings + batchInUse bool batchCancel context.CancelFunc batchReader *bufio.Reader batchWriter WriteCloserError + checkInUse bool checkCancel context.CancelFunc checkReader *bufio.Reader checkWriter WriteCloserError @@ -52,7 +54,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { if err != nil { return nil, err } else if !isDir(repoPath) { - return nil, errors.New("no such file or directory") + return nil, util.NewNotExistErrorf("no such file or directory") } // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! @@ -69,34 +71,34 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath) repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath) - repo.objectFormat, err = repo.GetObjectFormat() - if err != nil { - return nil, err - } - return repo, nil } // CatFileBatch obtains a CatFileBatch for this repository func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) { - if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 { + if repo.batchCancel == nil || repo.batchInUse { log.Debug("Opening temporary cat file batch for: %s", repo.Path) return CatFileBatch(ctx, repo.Path) } - return repo.batchWriter, repo.batchReader, func() {} + repo.batchInUse = true + return repo.batchWriter, repo.batchReader, func() { + repo.batchInUse = false + } } // CatFileBatchCheck obtains a CatFileBatchCheck for this repository func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func()) { - if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 { - log.Debug("Opening temporary cat file batch-check: %s", repo.Path) + if repo.checkCancel == nil || repo.checkInUse { + log.Debug("Opening temporary cat file batch-check for: %s", repo.Path) return CatFileBatchCheck(ctx, repo.Path) } - return repo.checkWriter, repo.checkReader, func() {} + repo.checkInUse = true + return repo.checkWriter, repo.checkReader, func() { + repo.checkInUse = false + } } -// Close this repository, in particular close the underlying gogitStorage if this is not nil -func (repo *Repository) Close() (err error) { +func (repo *Repository) Close() error { if repo == nil { return nil } @@ -105,14 +107,16 @@ func (repo *Repository) Close() (err error) { repo.batchReader = nil repo.batchWriter = nil repo.batchCancel = nil + repo.batchInUse = false } if repo.checkCancel != nil { repo.checkCancel() repo.checkCancel = nil repo.checkReader = nil repo.checkWriter = nil + repo.checkInUse = false } repo.LastCommitCache = nil repo.tagCache = nil - return err + return nil } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 4d5f06dca4..12f88feb9a 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -5,6 +5,7 @@ package git import ( + "context" "errors" "fmt" "strings" @@ -43,15 +44,8 @@ func (repo *Repository) GetHEADBranch() (*Branch, error) { }, nil } -// SetDefaultBranch sets default branch of repository. -func (repo *Repository) SetDefaultBranch(name string) error { - _, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").AddDynamicArguments(BranchPrefix + name).RunStdString(&RunOpts{Dir: repo.Path}) - return err -} - -// GetDefaultBranch gets default branch of repository. -func (repo *Repository) GetDefaultBranch() (string, error) { - stdout, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repo.Path}) +func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) { + stdout, _, err := NewCommand(ctx, "symbolic-ref", "HEAD").RunStdString(&RunOpts{Dir: repoPath}) if err != nil { return "", err } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 9c9ee7768f..44273d2253 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -246,7 +246,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) } }() - len := repo.objectFormat.FullLength() + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + + len := objectFormat.FullLength() commits := []*Commit{} shaline := make([]byte, len+1) for { diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 4cab957564..84580be9a5 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -41,7 +41,10 @@ func (repo *Repository) RemoveReference(name string) error { // ConvertToHash returns a Hash object from a potential ID string func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { - objectFormat := repo.objectFormat + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } if len(commitID) == hash.HexSize && objectFormat.IsValid(commitID) { ID, err := NewIDFromString(commitID) if err == nil { diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index f0214e1ff8..ae4c21aaa3 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -121,8 +121,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) return commit, nil default: log.Debug("Unknown typ: %s", typ) - _, err = rd.Discard(int(size) + 1) - if err != nil { + if err := DiscardFull(rd, size+1); err != nil { return nil, err } return nil, ErrNotExist{ @@ -133,8 +132,11 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) // ConvertToGitID returns a GitHash object from a potential ID string func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { - IDType := repo.objectFormat - if len(commitID) == IDType.FullLength() && IDType.IsValid(commitID) { + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) { ID, err := NewIDFromString(commitID) if err == nil { return ID, nil @@ -143,7 +145,7 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx) defer cancel() - _, err := wr.Write([]byte(commitID + "\n")) + _, err = wr.Write([]byte(commitID + "\n")) if err != nil { return nil, err } diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 0e9a0c70d7..b6e9d2b44a 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -283,8 +283,12 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error { // If base is undefined empty SHA (zeros), it only returns the files changed in the head commit // If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) { + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } cmd := NewCommand(repo.Ctx, "diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z") - if base == repo.objectFormat.EmptyObjectID().String() { + if base == objectFormat.EmptyObjectID().String() { cmd.AddDynamicArguments(head) } else { cmd.AddDynamicArguments(base, head) diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 526b213550..9983873186 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -126,17 +126,20 @@ func TestGetCommitFilesChanged(t *testing.T) { assert.NoError(t, err) defer repo.Close() + objectFormat, err := repo.GetObjectFormat() + assert.NoError(t, err) + testCases := []struct { base, head string files []string }{ { - repo.objectFormat.EmptyObjectID().String(), + objectFormat.EmptyObjectID().String(), "95bb4d39648ee7e325106df01a621c530863a653", []string{"file1.txt"}, }, { - repo.objectFormat.EmptyObjectID().String(), + objectFormat.EmptyObjectID().String(), "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", []string{"file2.txt"}, }, @@ -146,7 +149,7 @@ func TestGetCommitFilesChanged(t *testing.T) { []string{"file2.txt"}, }, { - repo.objectFormat.EmptyTree().String(), + objectFormat.EmptyTree().String(), "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", []string{"file1.txt", "file2.txt"}, }, diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index 47705a92af..6aaab242c1 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -94,6 +94,10 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { // RemoveFilesFromIndex removes given filenames from the index - it does not check whether they are present. func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return err + } cmd := NewCommand(repo.Ctx, "update-index", "--remove", "-z", "--index-info") stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) @@ -101,7 +105,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { for _, file := range filenames { if file != "" { buffer.WriteString("0 ") - buffer.WriteString(repo.objectFormat.EmptyObjectID().String()) + buffer.WriteString(objectFormat.EmptyObjectID().String()) buffer.WriteByte('\t') buffer.WriteString(file) buffer.WriteByte('\000') diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go index c40d6937b5..8551ea9d24 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/repo_language_stats.go @@ -6,6 +6,8 @@ package git import ( "strings" "unicode" + + "code.gitea.io/gitea/modules/optional" ) const ( @@ -46,3 +48,20 @@ func mergeLanguageStats(stats map[string]int64) map[string]int64 { } return res } + +func TryReadLanguageAttribute(attrs map[string]string) optional.Option[string] { + language := AttributeToString(attrs, AttributeLinguistLanguage) + if language.Value() == "" { + language = AttributeToString(attrs, AttributeGitlabLanguage) + if language.Has() { + raw := language.Value() + // gitlab-language may have additional parameters after the language + // ignore them and just use the main language + // https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type + if idx := strings.IndexByte(raw, '?'); idx >= 0 { + language = optional.Some(raw[:idx]) + } + } + } + return language +} diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go index 4c6fbd6c7e..a34c03c781 100644 --- a/modules/git/repo_language_stats_gogit.go +++ b/modules/git/repo_language_stats_gogit.go @@ -8,9 +8,9 @@ package git import ( "bytes" "io" - "strings" "code.gitea.io/gitea/modules/analyze" + "code.gitea.io/gitea/modules/optional" "github.com/go-enry/go-enry/v2" "github.com/go-git/go-git/v5" @@ -57,25 +57,38 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil } - notVendored := false - notGenerated := false + isVendored := optional.None[bool]() + isGenerated := optional.None[bool]() + isDocumentation := optional.None[bool]() + isDetectable := optional.None[bool]() if checker != nil { attrs, err := checker.CheckPath(f.Name) if err == nil { - if vendored, has := attrs["linguist-vendored"]; has { - if vendored == "set" || vendored == "true" { - return nil - } - notVendored = vendored == "false" + isVendored = AttributeToBool(attrs, AttributeLinguistVendored) + if isVendored.ValueOrDefault(false) { + return nil } - if generated, has := attrs["linguist-generated"]; has { - if generated == "set" || generated == "true" { - return nil - } - notGenerated = generated == "false" + + isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated) + if isGenerated.ValueOrDefault(false) { + return nil } - if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { + + isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation) + if isDocumentation.ValueOrDefault(false) { + return nil + } + + isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable) + if !isDetectable.ValueOrDefault(true) { + return nil + } + + hasLanguage := TryReadLanguageAttribute(attrs) + if hasLanguage.Value() != "" { + language := hasLanguage.Value() + // group languages, such as Pug -> HTML; SCSS -> CSS group := enry.GetLanguageGroup(language) if len(group) != 0 { @@ -85,28 +98,14 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err // this language will always be added to the size sizes[language] += f.Size return nil - } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" { - // strip off a ? if present - if idx := strings.IndexByte(language, '?'); idx >= 0 { - language = language[:idx] - } - if len(language) != 0 { - // group languages, such as Pug -> HTML; SCSS -> CSS - group := enry.GetLanguageGroup(language) - if len(group) != 0 { - language = group - } - - // this language will always be added to the size - sizes[language] += f.Size - return nil - } } } } - if (!notVendored && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) || - enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) { + if (!isVendored.Has() && analyze.IsVendor(f.Name)) || + enry.IsDotFile(f.Name) || + (!isDocumentation.Has() && enry.IsDocumentation(f.Name)) || + enry.IsConfiguration(f.Name) { return nil } @@ -115,12 +114,10 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err if f.Size <= bigFileSize { content, _ = readFile(f, fileSizeLimit) } - if !notGenerated && enry.IsGenerated(f.Name, content) { + if !isGenerated.Has() && enry.IsGenerated(f.Name, content) { return nil } - // TODO: Use .gitattributes file for linguist overrides - language := analyze.GetCodeLanguage(f.Name, content) if language == enry.OtherLanguage || language == "" { return nil @@ -138,7 +135,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err included = langtype == enry.Programming || langtype == enry.Markup includedLanguage[language] = included } - if included { + if included || isDetectable.ValueOrDefault(false) { sizes[language] += f.Size } else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) { firstExcludedLanguage = language diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go index 1d94ad6c00..318fc091ce 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/repo_language_stats_nogogit.go @@ -6,14 +6,12 @@ package git import ( - "bufio" "bytes" "io" - "math" - "strings" "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "github.com/go-enry/go-enry/v2" ) @@ -90,25 +88,38 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err continue } - notVendored := false - notGenerated := false + isVendored := optional.None[bool]() + isGenerated := optional.None[bool]() + isDocumentation := optional.None[bool]() + isDetectable := optional.None[bool]() if checker != nil { attrs, err := checker.CheckPath(f.Name()) if err == nil { - if vendored, has := attrs["linguist-vendored"]; has { - if vendored == "set" || vendored == "true" { - continue - } - notVendored = vendored == "false" + isVendored = AttributeToBool(attrs, AttributeLinguistVendored) + if isVendored.ValueOrDefault(false) { + continue } - if generated, has := attrs["linguist-generated"]; has { - if generated == "set" || generated == "true" { - continue - } - notGenerated = generated == "false" + + isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated) + if isGenerated.ValueOrDefault(false) { + continue } - if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { + + isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation) + if isDocumentation.ValueOrDefault(false) { + continue + } + + isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable) + if !isDetectable.ValueOrDefault(true) { + continue + } + + hasLanguage := TryReadLanguageAttribute(attrs) + if hasLanguage.Value() != "" { + language := hasLanguage.Value() + // group languages, such as Pug -> HTML; SCSS -> CSS group := enry.GetLanguageGroup(language) if len(group) != 0 { @@ -118,29 +129,14 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err // this language will always be added to the size sizes[language] += f.Size() continue - } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" { - // strip off a ? if present - if idx := strings.IndexByte(language, '?'); idx >= 0 { - language = language[:idx] - } - if len(language) != 0 { - // group languages, such as Pug -> HTML; SCSS -> CSS - group := enry.GetLanguageGroup(language) - if len(group) != 0 { - language = group - } - - // this language will always be added to the size - sizes[language] += f.Size() - continue - } } - } } - if (!notVendored && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) || - enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) { + if (!isVendored.Has() && analyze.IsVendor(f.Name())) || + enry.IsDotFile(f.Name()) || + (!isDocumentation.Has() && enry.IsDocumentation(f.Name())) || + enry.IsConfiguration(f.Name()) { continue } @@ -168,12 +164,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return nil, err } content = contentBuf.Bytes() - err = discardFull(batchReader, discard) - if err != nil { + if err := DiscardFull(batchReader, discard); err != nil { return nil, err } } - if !notGenerated && enry.IsGenerated(f.Name(), content) { + if !isGenerated.Has() && enry.IsGenerated(f.Name(), content) { continue } @@ -196,13 +191,12 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err included = langType == enry.Programming || langType == enry.Markup includedLanguage[language] = included } - if included { + if included || isDetectable.ValueOrDefault(false) { sizes[language] += f.Size() } else if len(sizes) == 0 && (firstExcludedLanguage == "" || firstExcludedLanguage == language) { firstExcludedLanguage = language firstExcludedLanguageSize += f.Size() } - continue } // If there are no included languages add the first excluded language @@ -212,21 +206,3 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err return mergeLanguageStats(sizes), nil } - -func discardFull(rd *bufio.Reader, discard int64) error { - if discard > math.MaxInt32 { - n, err := rd.Discard(math.MaxInt32) - discard -= int64(n) - if err != nil { - return err - } - } - for discard > 0 { - n, err := rd.Discard(int(discard)) - discard -= int64(n) - if err != nil { - return err - } - } - return nil -} diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index 41f94e24f9..83220104bd 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -124,6 +124,10 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) } } } + if err = scanner.Err(); err != nil { + _ = stdoutReader.Close() + return fmt.Errorf("GetCodeActivityStats scan: %w", err) + } a := make([]*CodeActivityAuthor, 0, len(authors)) for _, v := range authors { a = append(a, v) diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 0262351a9a..818ac8c95c 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -135,7 +135,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { break } - tag, err := parseTagRef(repo.objectFormat, ref) + tag, err := parseTagRef(ref) if err != nil { return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err) } @@ -155,7 +155,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { } // parseTagRef parses a tag from a 'git for-each-ref'-produced reference. -func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, err error) { +func parseTagRef(ref map[string]string) (tag *Tag, err error) { tag = &Tag{ Type: ref["objecttype"], Name: ref["refname:lstrip=2"], @@ -177,23 +177,17 @@ func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, er } } - tag.Tagger, err = newSignatureFromCommitline([]byte(ref["creator"])) - if err != nil { - return nil, fmt.Errorf("parse tagger: %w", err) - } - + tag.Tagger = parseSignatureFromCommitLine(ref["creator"]) tag.Message = ref["contents"] - // strip PGP signature if present in contents field - pgpStart := strings.Index(tag.Message, beginpgp) - if pgpStart >= 0 { - tag.Message = tag.Message[0:pgpStart] - } + + // strip any signature if present in contents field + _, tag.Message, _ = parsePayloadSignature(util.UnsafeStringToBytes(tag.Message), 0) // annotated tag with GPG signature if tag.Type == "tag" && ref["contents:signature"] != "" { payload := fmt.Sprintf("object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n", tag.Object, tag.Name, ref["creator"], strings.TrimSpace(tag.Message)) - tag.Signature = &CommitGPGSignature{ + tag.Signature = &CommitSignature{ Signature: ref["contents:signature"], Payload: payload, } diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 5d98fadd54..cbab39f8c5 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -103,6 +103,9 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { return nil, err } if typ != "tag" { + if err := DiscardFull(rd, size+1); err != nil { + return nil, err + } return nil, ErrNotExist{ID: tagID.String()} } diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index da7b1455a8..0117cb902d 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -194,7 +194,6 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { } func TestRepository_parseTagRef(t *testing.T) { - sha1 := Sha1ObjectFormat tests := []struct { name string @@ -227,7 +226,7 @@ func TestRepository_parseTagRef(t *testing.T) { ID: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), Object: MustIDFromString("ab23e4b7f4cd0caafe0174c0e7ef6d651ba72889"), Type: "commit", - Tagger: parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"), + Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"), Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", Signature: nil, }, @@ -256,7 +255,7 @@ func TestRepository_parseTagRef(t *testing.T) { ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), Type: "tag", - Tagger: parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"), + Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"), Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md\n", Signature: nil, }, @@ -314,9 +313,9 @@ qbHDASXl ID: MustIDFromString("8c68a1f06fc59c655b7e3905b159d761e91c53c9"), Object: MustIDFromString("3325fd8a973321fd59455492976c042dde3fd1ca"), Type: "tag", - Tagger: parseAuthorLine(t, "Foo Bar <foo@bar.com> 1565789218 +0300"), + Tagger: parseSignatureFromCommitLine("Foo Bar <foo@bar.com> 1565789218 +0300"), Message: "Add changelog of v1.9.1 (#7859)\n\n* add changelog of v1.9.1\n* Update CHANGELOG.md", - Signature: &CommitGPGSignature{ + Signature: &CommitSignature{ Signature: `-----BEGIN PGP SIGNATURE----- aBCGzBAABCgAdFiEEyWRwv/q1Q6IjSv+D4IPOwzt33PoFAmI8jbIACgkQ4IPOwzt3 @@ -351,7 +350,7 @@ Add changelog of v1.9.1 (#7859) for _, test := range tests { tc := test // don't close over loop variable t.Run(tc.name, func(t *testing.T) { - got, err := parseTagRef(sha1, tc.givenRef) + got, err := parseTagRef(tc.givenRef) if tc.wantErr { require.Error(t, err) @@ -363,14 +362,3 @@ Add changelog of v1.9.1 (#7859) }) } } - -func parseAuthorLine(t *testing.T, committer string) *Signature { - t.Helper() - - sig, err := newSignatureFromCommitline([]byte(committer)) - if err != nil { - t.Fatalf("parse author line '%s': %v", committer, err) - } - - return sig -} diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index 6391959e6a..dc97ce1344 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -21,7 +21,12 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { // GetTree find the tree object in the repository. func (repo *Repository) GetTree(idStr string) (*Tree, error) { - if len(idStr) != repo.objectFormat.FullLength() { + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + + if len(idStr) != objectFormat.FullLength() { res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err diff --git a/modules/git/repo_tree_nogogit.go b/modules/git/repo_tree_nogogit.go index 20c92a79ed..e82012de6f 100644 --- a/modules/git/repo_tree_nogogit.go +++ b/modules/git/repo_tree_nogogit.go @@ -51,13 +51,20 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { case "tree": tree := NewTree(repo, id) tree.ResolvedID = id - tree.entries, err = catBatchParseTreeEntries(repo.objectFormat, tree, rd, size) + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, size) if err != nil { return nil, err } tree.entriesParsed = true return tree, nil default: + if err := DiscardFull(rd, size+1); err != nil { + return nil, err + } return nil, ErrNotExist{ ID: id.String(), } @@ -66,7 +73,11 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { // GetTree find the tree object in the repository. func (repo *Repository) GetTree(idStr string) (*Tree, error) { - if len(idStr) != repo.objectFormat.FullLength() { + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + if len(idStr) != objectFormat.FullLength() { res, err := repo.GetRefCommitID(idStr) if err != nil { return nil, err diff --git a/modules/git/signature.go b/modules/git/signature.go index b5b17f23b0..f50a097758 100644 --- a/modules/git/signature.go +++ b/modules/git/signature.go @@ -4,7 +4,46 @@ package git -const ( - // GitTimeLayout is the (default) time layout used by git. - GitTimeLayout = "Mon Jan _2 15:04:05 2006 -0700" +import ( + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" ) + +// Helper to get a signature from the commit line, which looks like: +// +// full name <user@example.com> 1378823654 +0200 +// +// Haven't found the official reference for the standard format yet. +// This function never fails, if the "line" can't be parsed, it returns a default Signature with "zero" time. +func parseSignatureFromCommitLine(line string) *Signature { + sig := &Signature{} + s1, sx, ok1 := strings.Cut(line, " <") + s2, s3, ok2 := strings.Cut(sx, "> ") + if !ok1 || !ok2 { + sig.Name = line + return sig + } + sig.Name, sig.Email = s1, s2 + + if strings.Count(s3, " ") == 1 { + ts, tz, _ := strings.Cut(s3, " ") + seconds, _ := strconv.ParseInt(ts, 10, 64) + if tzTime, err := time.Parse("-0700", tz); err == nil { + sig.When = time.Unix(seconds, 0).In(tzTime.Location()) + } + } else { + // the old gitea code tried to parse the date in a few different formats, but it's not clear why. + // according to public document, only the standard format "timestamp timezone" could be found, so drop other formats. + log.Error("suspicious commit line format: %q", line) + for _, fmt := range []string{ /*"Mon Jan _2 15:04:05 2006 -0700"*/ } { + if t, err := time.Parse(fmt, s3); err == nil { + sig.When = t + break + } + } + } + return sig +} diff --git a/modules/git/signature_gogit.go b/modules/git/signature_gogit.go index c984ad6e20..1fc6aabceb 100644 --- a/modules/git/signature_gogit.go +++ b/modules/git/signature_gogit.go @@ -7,52 +7,8 @@ package git import ( - "bytes" - "strconv" - "strings" - "time" - "github.com/go-git/go-git/v5/plumbing/object" ) // Signature represents the Author or Committer information. type Signature = object.Signature - -// Helper to get a signature from the commit line, which looks like these: -// -// author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200 -// author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200 -// -// but without the "author " at the beginning (this method should) -// be used for author and committer. -// -// FIXME: include timezone for timestamp! -func newSignatureFromCommitline(line []byte) (_ *Signature, err error) { - sig := new(Signature) - emailStart := bytes.IndexByte(line, '<') - if emailStart > 0 { // Empty name has already occurred, even if it shouldn't - sig.Name = strings.TrimSpace(string(line[:emailStart-1])) - } - emailEnd := bytes.IndexByte(line, '>') - sig.Email = string(line[emailStart+1 : emailEnd]) - - // Check date format. - if len(line) > emailEnd+2 { - firstChar := line[emailEnd+2] - if firstChar >= 48 && firstChar <= 57 { - timestop := bytes.IndexByte(line[emailEnd+2:], ' ') - timestring := string(line[emailEnd+2 : emailEnd+2+timestop]) - seconds, _ := strconv.ParseInt(timestring, 10, 64) - sig.When = time.Unix(seconds, 0) - } else { - sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:])) - if err != nil { - return nil, err - } - } - } else { - // Fall back to unix 0 time - sig.When = time.Unix(0, 0) - } - return sig, nil -} diff --git a/modules/git/signature_nogogit.go b/modules/git/signature_nogogit.go index 25277f99d5..0d19c0abdc 100644 --- a/modules/git/signature_nogogit.go +++ b/modules/git/signature_nogogit.go @@ -7,21 +7,17 @@ package git import ( - "bytes" "fmt" - "strconv" - "strings" "time" + + "code.gitea.io/gitea/modules/util" ) -// Signature represents the Author or Committer information. +// Signature represents the Author, Committer or Tagger information. type Signature struct { - // Name represents a person name. It is an arbitrary string. - Name string - // Email is an email, but it cannot be assumed to be well-formed. - Email string - // When is the timestamp of the signature. - When time.Time + Name string // the committer name, it can be anything + Email string // the committer email, it can be anything + When time.Time // the timestamp of the signature } func (s *Signature) String() string { @@ -30,71 +26,5 @@ func (s *Signature) String() string { // Decode decodes a byte array representing a signature to signature func (s *Signature) Decode(b []byte) { - sig, _ := newSignatureFromCommitline(b) - s.Email = sig.Email - s.Name = sig.Name - s.When = sig.When -} - -// Helper to get a signature from the commit line, which looks like these: -// -// author Patrick Gundlach <gundlach@speedata.de> 1378823654 +0200 -// author Patrick Gundlach <gundlach@speedata.de> Thu, 07 Apr 2005 22:13:13 +0200 -// -// but without the "author " at the beginning (this method should) -// be used for author and committer. -// FIXME: there are a lot of "return sig, err" (but the err is also nil), that's the old behavior, to avoid breaking -func newSignatureFromCommitline(line []byte) (sig *Signature, err error) { - sig = new(Signature) - emailStart := bytes.LastIndexByte(line, '<') - emailEnd := bytes.LastIndexByte(line, '>') - if emailStart == -1 || emailEnd == -1 || emailEnd < emailStart { - return sig, err - } - - if emailStart > 0 { // Empty name has already occurred, even if it shouldn't - sig.Name = strings.TrimSpace(string(line[:emailStart-1])) - } - sig.Email = string(line[emailStart+1 : emailEnd]) - - hasTime := emailEnd+2 < len(line) - if !hasTime { - return sig, err - } - - // Check date format. - firstChar := line[emailEnd+2] - if firstChar >= 48 && firstChar <= 57 { - idx := bytes.IndexByte(line[emailEnd+2:], ' ') - if idx < 0 { - return sig, err - } - - timestring := string(line[emailEnd+2 : emailEnd+2+idx]) - seconds, _ := strconv.ParseInt(timestring, 10, 64) - sig.When = time.Unix(seconds, 0) - - idx += emailEnd + 3 - if idx >= len(line) || idx+5 > len(line) { - return sig, err - } - - timezone := string(line[idx : idx+5]) - tzhours, err1 := strconv.ParseInt(timezone[0:3], 10, 64) - tzmins, err2 := strconv.ParseInt(timezone[3:], 10, 64) - if err1 != nil || err2 != nil { - return sig, err - } - if tzhours < 0 { - tzmins *= -1 - } - tz := time.FixedZone("", int(tzhours*60*60+tzmins*60)) - sig.When = sig.When.In(tz) - } else { - sig.When, err = time.Parse(GitTimeLayout, string(line[emailEnd+2:])) - if err != nil { - return sig, err - } - } - return sig, err + *s = *parseSignatureFromCommitLine(util.UnsafeBytesToString(b)) } diff --git a/modules/git/signature_test.go b/modules/git/signature_test.go new file mode 100644 index 0000000000..92681feea9 --- /dev/null +++ b/modules/git/signature_test.go @@ -0,0 +1,47 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestParseSignatureFromCommitLine(t *testing.T) { + tests := []struct { + line string + want *Signature + }{ + { + line: "a b <c@d.com> 12345 +0100", + want: &Signature{ + Name: "a b", + Email: "c@d.com", + When: time.Unix(12345, 0).In(time.FixedZone("", 3600)), + }, + }, + { + line: "bad line", + want: &Signature{Name: "bad line"}, + }, + { + line: "bad < line", + want: &Signature{Name: "bad < line"}, + }, + { + line: "bad > line", + want: &Signature{Name: "bad > line"}, + }, + { + line: "bad-line <name@example.com>", + want: &Signature{Name: "bad-line <name@example.com>"}, + }, + } + for _, test := range tests { + got := parseSignatureFromCommitLine(test.line) + assert.EqualValues(t, test.want, got) + } +} diff --git a/modules/git/tag.go b/modules/git/tag.go index 01a8d6f6a5..f7666aa89b 100644 --- a/modules/git/tag.go +++ b/modules/git/tag.go @@ -6,12 +6,8 @@ package git import ( "bytes" "sort" - "strings" -) -const ( - beginpgp = "\n-----BEGIN PGP SIGNATURE-----\n" - endpgp = "\n-----END PGP SIGNATURE-----" + "code.gitea.io/gitea/modules/util" ) // Tag represents a Git tag. @@ -22,7 +18,7 @@ type Tag struct { Type string Tagger *Signature Message string - Signature *CommitGPGSignature + Signature *CommitSignature } // Commit return the commit of the tag reference @@ -30,6 +26,36 @@ func (tag *Tag) Commit(gitRepo *Repository) (*Commit, error) { return gitRepo.getCommit(tag.Object) } +func parsePayloadSignature(data []byte, messageStart int) (payload, msg, sign string) { + pos := messageStart + signStart, signEnd := -1, -1 + for { + eol := bytes.IndexByte(data[pos:], '\n') + if eol < 0 { + break + } + line := data[pos : pos+eol] + signType, hasPrefix := bytes.CutPrefix(line, []byte("-----BEGIN ")) + signType, hasSuffix := bytes.CutSuffix(signType, []byte(" SIGNATURE-----")) + if hasPrefix && hasSuffix { + signEndBytes := append([]byte("\n-----END "), signType...) + signEndBytes = append(signEndBytes, []byte(" SIGNATURE-----")...) + signEnd = bytes.Index(data[pos:], signEndBytes) + if signEnd != -1 { + signStart = pos + signEnd = pos + signEnd + len(signEndBytes) + } + } + pos += eol + 1 + } + + if signStart != -1 && signEnd != -1 { + msgEnd := max(messageStart, signStart-1) + return string(data[:msgEnd]), string(data[messageStart:msgEnd]), string(data[signStart:signEnd]) + } + return string(data), string(data[messageStart:]), "" +} + // Parse commit information from the (uncompressed) raw // data from the commit object. // \n\n separate headers from message @@ -38,51 +64,37 @@ func parseTagData(objectFormat ObjectFormat, data []byte) (*Tag, error) { tag.ID = objectFormat.EmptyObjectID() tag.Object = objectFormat.EmptyObjectID() tag.Tagger = &Signature{} - // we now have the contents of the commit object. Let's investigate... - nextline := 0 -l: + + pos := 0 for { - eol := bytes.IndexByte(data[nextline:], '\n') - switch { - case eol > 0: - line := data[nextline : nextline+eol] - spacepos := bytes.IndexByte(line, ' ') - reftype := line[:spacepos] - switch string(reftype) { - case "object": - id, err := NewIDFromString(string(line[spacepos+1:])) - if err != nil { - return nil, err - } - tag.Object = id - case "type": - // A commit can have one or more parents - tag.Type = string(line[spacepos+1:]) - case "tagger": - sig, err := newSignatureFromCommitline(line[spacepos+1:]) - if err != nil { - return nil, err - } - tag.Tagger = sig - } - nextline += eol + 1 - case eol == 0: - tag.Message = string(data[nextline+1:]) - break l - default: - break l + eol := bytes.IndexByte(data[pos:], '\n') + if eol == -1 { + break // shouldn't happen, but could just tolerate it } + if eol == 0 { + pos++ + break // end of headers + } + line := data[pos : pos+eol] + key, val, _ := bytes.Cut(line, []byte(" ")) + switch string(key) { + case "object": + id, err := NewIDFromString(string(val)) + if err != nil { + return nil, err + } + tag.Object = id + case "type": + tag.Type = string(val) // A commit can have one or more parents + case "tagger": + tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(val)) + } + pos += eol + 1 } - idx := strings.LastIndex(tag.Message, beginpgp) - if idx > 0 { - endSigIdx := strings.Index(tag.Message[idx:], endpgp) - if endSigIdx > 0 { - tag.Signature = &CommitGPGSignature{ - Signature: tag.Message[idx+1 : idx+endSigIdx+len(endpgp)], - Payload: string(data[:bytes.LastIndex(data, []byte(beginpgp))+1]), - } - tag.Message = tag.Message[:idx+1] - } + payload, msg, sign := parsePayloadSignature(data, pos) + tag.Message = msg + if len(sign) > 0 { + tag.Signature = &CommitSignature{Signature: sign, Payload: payload} } return tag, nil } diff --git a/modules/git/tag_test.go b/modules/git/tag_test.go index f980b0c560..ba02c28946 100644 --- a/modules/git/tag_test.go +++ b/modules/git/tag_test.go @@ -12,24 +12,28 @@ import ( func Test_parseTagData(t *testing.T) { testData := []struct { - data []byte - tag Tag + data string + expected Tag }{ - {data: []byte(`object 3b114ab800c6432ad42387ccf6bc8d4388a2885a + { + data: `object 3b114ab800c6432ad42387ccf6bc8d4388a2885a type commit tag 1.22.0 tagger Lucas Michot <lucas@semalead.com> 1484491741 +0100 -`), tag: Tag{ - Name: "", - ID: Sha1ObjectFormat.EmptyObjectID(), - Object: &Sha1Hash{0x3b, 0x11, 0x4a, 0xb8, 0x0, 0xc6, 0x43, 0x2a, 0xd4, 0x23, 0x87, 0xcc, 0xf6, 0xbc, 0x8d, 0x43, 0x88, 0xa2, 0x88, 0x5a}, - Type: "commit", - Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0)}, - Message: "", - Signature: nil, - }}, - {data: []byte(`object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc +`, + expected: Tag{ + Name: "", + ID: Sha1ObjectFormat.EmptyObjectID(), + Object: MustIDFromString("3b114ab800c6432ad42387ccf6bc8d4388a2885a"), + Type: "commit", + Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))}, + Message: "", + Signature: nil, + }, + }, + { + data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc type commit tag 1.22.1 tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100 @@ -37,37 +41,57 @@ tagger Lucas Michot <lucas@semalead.com> 1484553735 +0100 test message o -ono`), tag: Tag{ - Name: "", - ID: Sha1ObjectFormat.EmptyObjectID(), - Object: &Sha1Hash{0x7c, 0xdf, 0x42, 0xc0, 0xb1, 0xcc, 0x76, 0x3a, 0xb7, 0xe4, 0xc3, 0x3c, 0x47, 0xa2, 0x4e, 0x27, 0xc6, 0x6b, 0xfc, 0xcc}, - Type: "commit", - Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0)}, - Message: "test message\no\n\nono", - Signature: nil, - }}, +ono`, + expected: Tag{ + Name: "", + ID: Sha1ObjectFormat.EmptyObjectID(), + Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"), + Type: "commit", + Tagger: &Signature{Name: "Lucas Michot", Email: "lucas@semalead.com", When: time.Unix(1484553735, 0).In(time.FixedZone("", 3600))}, + Message: "test message\no\n\nono", + Signature: nil, + }, + }, + { + data: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa +type commit +tag v0 +tagger dummy user <dummy-email@example.com> 1484491741 +0100 + +dummy message +-----BEGIN SSH SIGNATURE----- +dummy signature +-----END SSH SIGNATURE----- +`, + expected: Tag{ + Name: "", + ID: Sha1ObjectFormat.EmptyObjectID(), + Object: MustIDFromString("7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa"), + Type: "commit", + Tagger: &Signature{Name: "dummy user", Email: "dummy-email@example.com", When: time.Unix(1484491741, 0).In(time.FixedZone("", 3600))}, + Message: "dummy message", + Signature: &CommitSignature{ + Signature: `-----BEGIN SSH SIGNATURE----- +dummy signature +-----END SSH SIGNATURE-----`, + Payload: `object 7cdf42c0b1cc763ab7e4c33c47a24e27c66bfaaa +type commit +tag v0 +tagger dummy user <dummy-email@example.com> 1484491741 +0100 + +dummy message`, + }, + }, + }, } for _, test := range testData { - tag, err := parseTagData(Sha1ObjectFormat, test.data) + tag, err := parseTagData(Sha1ObjectFormat, []byte(test.data)) assert.NoError(t, err) - assert.EqualValues(t, test.tag.ID, tag.ID) - assert.EqualValues(t, test.tag.Object, tag.Object) - assert.EqualValues(t, test.tag.Name, tag.Name) - assert.EqualValues(t, test.tag.Message, tag.Message) - assert.EqualValues(t, test.tag.Type, tag.Type) - if test.tag.Signature != nil && assert.NotNil(t, tag.Signature) { - assert.EqualValues(t, test.tag.Signature.Signature, tag.Signature.Signature) - assert.EqualValues(t, test.tag.Signature.Payload, tag.Signature.Payload) - } else { - assert.Nil(t, tag.Signature) - } - if test.tag.Tagger != nil && assert.NotNil(t, tag.Tagger) { - assert.EqualValues(t, test.tag.Tagger.Name, tag.Tagger.Name) - assert.EqualValues(t, test.tag.Tagger.Email, tag.Tagger.Email) - assert.EqualValues(t, test.tag.Tagger.When.Unix(), tag.Tagger.When.Unix()) - } else { - assert.Nil(t, tag.Tagger) - } + assert.Equal(t, test.expected, *tag) } + + tag, err := parseTagData(Sha1ObjectFormat, []byte("type commit\n\nfoo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...")) + assert.NoError(t, err) + assert.Equal(t, "foo\n-----BEGIN SSH SIGNATURE-----\ncorrupted...", tag.Message) } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 89d3aebbc0..a591485082 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -7,7 +7,6 @@ package git import ( "io" - "math" "strings" ) @@ -63,19 +62,8 @@ func (t *Tree) ListEntries() (Entries, error) { } // Not a tree just use ls-tree instead - for sz > math.MaxInt32 { - discarded, err := rd.Discard(math.MaxInt32) - sz -= int64(discarded) - if err != nil { - return nil, err - } - } - for sz > 0 { - discarded, err := rd.Discard(int(sz)) - sz -= int64(discarded) - if err != nil { - return nil, err - } + if err := DiscardFull(rd, sz+1); err != nil { + return nil, err } } @@ -89,8 +77,11 @@ func (t *Tree) ListEntries() (Entries, error) { return nil, runErr } - var err error - t.entries, err = parseTreeEntries(t.repo.objectFormat, stdout, t) + objectFormat, err := t.repo.GetObjectFormat() + if err != nil { + return nil, err + } + t.entries, err = parseTreeEntries(objectFormat, stdout, t) if err == nil { t.entriesParsed = true } @@ -113,8 +104,11 @@ func (t *Tree) listEntriesRecursive(extraArgs TrustedCmdArgs) (Entries, error) { return nil, runErr } - var err error - t.entriesRecursive, err = parseTreeEntries(t.repo.objectFormat, stdout, t) + objectFormat, err := t.repo.GetObjectFormat() + if err != nil { + return nil, err + } + t.entriesRecursive, err = parseTreeEntries(objectFormat, stdout, t) if err == nil { t.entriesRecursiveParsed = true } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go new file mode 100644 index 0000000000..6d2b5c84d5 --- /dev/null +++ b/modules/git/tree_test.go @@ -0,0 +1,27 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubTree_Issue29101(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) + assert.NoError(t, err) + defer repo.Close() + + commit, err := repo.GetCommit("ce064814f4a0d337b333e646ece456cd39fab612") + assert.NoError(t, err) + + // old code could produce a different error if called multiple times + for i := 0; i < 10; i++ { + _, err = commit.SubTree("file1.txt") + assert.Error(t, err) + assert.True(t, IsErrNotExist(err)) + } +} diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go index dcd9c391ef..bb79fb95ca 100644 --- a/modules/gitrepo/branch.go +++ b/modules/gitrepo/branch.go @@ -50,3 +50,20 @@ func IsBranchExist(ctx context.Context, repo Repository, name string) bool { func IsWikiBranchExist(ctx context.Context, repo Repository, name string) bool { return IsWikiReferenceExist(ctx, repo, git.BranchPrefix+name) } + +// SetDefaultBranch sets default branch of repository. +func SetDefaultBranch(ctx context.Context, repo Repository, name string) error { + _, _, err := git.NewCommand(ctx, "symbolic-ref", "HEAD"). + AddDynamicArguments(git.BranchPrefix + name). + RunStdString(&git.RunOpts{Dir: repoPath(repo)}) + return err +} + +// GetDefaultBranch gets default branch of repository. +func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) { + return git.GetDefaultBranch(ctx, repoPath(repo)) +} + +func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) { + return git.GetDefaultBranch(ctx, wikiPath(repo)) +} diff --git a/modules/gitrepo/hooks.go b/modules/gitrepo/hooks.go index 9f983247d4..5239fa4384 100644 --- a/modules/gitrepo/hooks.go +++ b/modules/gitrepo/hooks.go @@ -10,7 +10,6 @@ import ( "path/filepath" "runtime" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -95,15 +94,14 @@ done `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), } - if git.SupportProcReceive { - hookNames = append(hookNames, "proc-receive") - hookTpls = append(hookTpls, - fmt.Sprintf(`#!/usr/bin/env %s + // although only new git (>=2.29) supports proc-receive, it's still good to create its hook, in case the user upgrades git + hookNames = append(hookNames, "proc-receive") + hookTpls = append(hookTpls, + fmt.Sprintf(`#!/usr/bin/env %s # AUTO GENERATED BY GITEA, DO NOT MODIFY %s hook --config=%s proc-receive `, setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf))) - giteaHookTpls = append(giteaHookTpls, "") - } + giteaHookTpls = append(giteaHookTpls, "") return hookNames, hookTpls, giteaHookTpls } diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index f3f412863a..3f1115066a 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -233,7 +233,10 @@ func (g *Manager) setStateTransition(old, new state) bool { // At the moment the total number of servers (numberOfServersToCreate) are pre-defined as a const before global init, // so this function MUST be called if a server is not used. func (g *Manager) InformCleanup() { - g.createServerWaitGroup.Done() + g.createServerCond.L.Lock() + defer g.createServerCond.L.Unlock() + g.createdServer++ + g.createServerCond.Signal() } // Done allows the manager to be viewed as a context.Context, it returns a channel that is closed when the server is finished terminating diff --git a/modules/graceful/manager_common.go b/modules/graceful/manager_common.go index 27196e1531..f6dbcc748d 100644 --- a/modules/graceful/manager_common.go +++ b/modules/graceful/manager_common.go @@ -42,8 +42,9 @@ type Manager struct { terminateCtxCancel context.CancelFunc managerCtxCancel context.CancelFunc runningServerWaitGroup sync.WaitGroup - createServerWaitGroup sync.WaitGroup terminateWaitGroup sync.WaitGroup + createServerCond sync.Cond + createdServer int shutdownRequested chan struct{} toRunAtShutdown []func() @@ -52,7 +53,7 @@ type Manager struct { func newGracefulManager(ctx context.Context) *Manager { manager := &Manager{ctx: ctx, shutdownRequested: make(chan struct{})} - manager.createServerWaitGroup.Add(numberOfServersToCreate) + manager.createServerCond.L = &sync.Mutex{} manager.prepare(ctx) manager.start() return manager diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go index f4af4993d9..d03fff9b5b 100644 --- a/modules/graceful/manager_unix.go +++ b/modules/graceful/manager_unix.go @@ -57,12 +57,27 @@ func (g *Manager) start() { // Handle clean up of unused provided listeners and delayed start-up startupDone := make(chan struct{}) go func() { - defer close(startupDone) - // Wait till we're done getting all the listeners and then close the unused ones - g.createServerWaitGroup.Wait() - // Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function - _ = CloseProvidedListeners() - g.notify(readyMsg) + defer func() { + close(startupDone) + // Close the unused listeners + closeProvidedListeners() + }() + // Wait for all servers to be created + g.createServerCond.L.Lock() + for { + if g.createdServer >= numberOfServersToCreate { + g.createServerCond.L.Unlock() + g.notify(readyMsg) + return + } + select { + case <-g.IsShutdown(): + g.createServerCond.L.Unlock() + return + default: + } + g.createServerCond.Wait() + } }() if setting.StartupTimeout > 0 { go func() { @@ -70,16 +85,7 @@ func (g *Manager) start() { case <-startupDone: return case <-g.IsShutdown(): - func() { - // When WaitGroup counter goes negative it will panic - we don't care about this so we can just ignore it. - defer func() { - _ = recover() - }() - // Ensure that the createServerWaitGroup stops waiting - for { - g.createServerWaitGroup.Done() - } - }() + g.createServerCond.Signal() return case <-time.After(setting.StartupTimeout): log.Error("Startup took too long! Shutting down") diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go index 0248dcb24d..d776e0e9f9 100644 --- a/modules/graceful/manager_windows.go +++ b/modules/graceful/manager_windows.go @@ -149,25 +149,35 @@ hammerLoop: func (g *Manager) awaitServer(limit time.Duration) bool { c := make(chan struct{}) go func() { - defer close(c) - g.createServerWaitGroup.Wait() + g.createServerCond.L.Lock() + for { + if g.createdServer >= numberOfServersToCreate { + g.createServerCond.L.Unlock() + close(c) + return + } + select { + case <-g.IsShutdown(): + g.createServerCond.L.Unlock() + return + default: + } + g.createServerCond.Wait() + } }() + + var tc <-chan time.Time if limit > 0 { - select { - case <-c: - return true // completed normally - case <-time.After(limit): - return false // timed out - case <-g.IsShutdown(): - return false - } - } else { - select { - case <-c: - return true // completed normally - case <-g.IsShutdown(): - return false - } + tc = time.After(limit) + } + select { + case <-c: + return true // completed normally + case <-tc: + return false // timed out + case <-g.IsShutdown(): + g.createServerCond.Signal() + return false } } diff --git a/modules/graceful/net_unix.go b/modules/graceful/net_unix.go index 4f8c036a69..796e00507c 100644 --- a/modules/graceful/net_unix.go +++ b/modules/graceful/net_unix.go @@ -129,25 +129,17 @@ func getProvidedFDs() (savedErr error) { return savedErr } -// CloseProvidedListeners closes all unused provided listeners. -func CloseProvidedListeners() error { +// closeProvidedListeners closes all unused provided listeners. +func closeProvidedListeners() { mutex.Lock() defer mutex.Unlock() - var returnableError error for _, l := range providedListeners { err := l.Close() if err != nil { log.Error("Error in closing unused provided listener: %v", err) - if returnableError != nil { - returnableError = fmt.Errorf("%v & %w", returnableError, err) - } else { - returnableError = err - } } } providedListeners = []net.Listener{} - - return returnableError } // DefaultGetListener obtains a listener for the stream-oriented local network address: diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 14b95898f5..903799cb68 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -8,20 +8,42 @@ import ( "strings" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) -// IsRiskyRedirectURL returns true if the URL is considered risky for redirects -func IsRiskyRedirectURL(s string) bool { +func urlIsRelative(s string, u *url.URL) bool { // Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH" // Therefore we should ignore these redirect locations to prevent open redirects if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') { - return true + return false } - - u, err := url.Parse(s) - if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(s), strings.ToLower(setting.AppURL))) { - return true - } - - return false + return u != nil && u.Scheme == "" && u.Host == "" +} + +// IsRelativeURL detects if a URL is relative (no scheme or host) +func IsRelativeURL(s string) bool { + u, err := url.Parse(s) + return err == nil && urlIsRelative(s, u) +} + +func IsCurrentGiteaSiteURL(s string) bool { + u, err := url.Parse(s) + if err != nil { + return false + } + if u.Path != "" { + cleanedPath := util.PathJoinRelX(u.Path) + if cleanedPath == "" || cleanedPath == "." { + u.Path = "/" + } else { + u.Path += "/" + cleanedPath + "/" + } + } + if urlIsRelative(s, u) { + return u.Path == "" || strings.HasPrefix(strings.ToLower(u.Path), strings.ToLower(setting.AppSubURL+"/")) + } + if u.Path == "" { + u.Path = "/" + } + return strings.HasPrefix(strings.ToLower(u.String()), strings.ToLower(setting.AppURL)) } diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index 72033b1208..9bf09bcf2f 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -7,32 +7,70 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) -func TestIsRiskyRedirectURL(t *testing.T) { - setting.AppURL = "http://localhost:3000/" - tests := []struct { - input string - want bool - }{ - {"", false}, - {"foo", false}, - {"/", false}, - {"/foo?k=%20#abc", false}, - - {"//", true}, - {"\\\\", true}, - {"/\\", true}, - {"\\/", true}, - {"mail:a@b.com", true}, - {"https://test.com", true}, - {setting.AppURL + "/foo", false}, +func TestIsRelativeURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + rel := []string{ + "", + "foo", + "/", + "/foo?k=%20#abc", } - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - assert.Equal(t, tt.want, IsRiskyRedirectURL(tt.input)) - }) + for _, s := range rel { + assert.True(t, IsRelativeURL(s), "rel = %q", s) + } + abs := []string{ + "//", + "\\\\", + "/\\", + "\\/", + "mailto:a@b.com", + "https://test.com", + } + for _, s := range abs { + assert.False(t, IsRelativeURL(s), "abs = %q", s) } } + +func TestIsCurrentGiteaSiteURL(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + good := []string{ + "?key=val", + "/sub", + "/sub/", + "/sub/foo", + "/sub/foo/", + "http://localhost:3000/sub?key=val", + "http://localhost:3000/sub/", + } + for _, s := range good { + assert.True(t, IsCurrentGiteaSiteURL(s), "good = %q", s) + } + bad := []string{ + ".", + "foo", + "/", + "//", + "\\\\", + "/foo", + "http://localhost:3000/sub/..", + "http://localhost:3000/other", + "http://other/", + } + for _, s := range bad { + assert.False(t, IsCurrentGiteaSiteURL(s), "bad = %q", s) + } + + setting.AppURL = "http://localhost:3000/" + setting.AppSubURL = "" + assert.False(t, IsCurrentGiteaSiteURL("//")) + assert.False(t, IsCurrentGiteaSiteURL("\\\\")) + assert.False(t, IsCurrentGiteaSiteURL("http://localhost")) + assert.True(t, IsCurrentGiteaSiteURL("http://localhost:3000?key=val")) +} diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index 8ba50ed77c..c607d780ef 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -39,6 +39,8 @@ import ( const ( unicodeNormalizeName = "unicodeNormalize" maxBatchSize = 16 + // fuzzyDenominator determines the levenshtein distance per each character of a keyword + fuzzyDenominator = 4 ) func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { @@ -142,7 +144,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro return err } if size, err = strconv.ParseInt(strings.TrimSpace(stdout), 10, 64); err != nil { - return fmt.Errorf("Misformatted git cat-file output: %w", err) + return fmt.Errorf("misformatted git cat-file output: %w", err) } } @@ -233,26 +235,23 @@ func (b *Indexer) Delete(_ context.Context, repoID int64) error { // Search searches for files in the specified repo. // Returns the matching file-paths -func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { +func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { var ( indexerQuery query.Query keywordQuery query.Query ) - if isMatch { - prefixQuery := bleve.NewPrefixQuery(keyword) - prefixQuery.FieldVal = "Content" - keywordQuery = prefixQuery - } else { - phraseQuery := bleve.NewMatchPhraseQuery(keyword) - phraseQuery.FieldVal = "Content" - phraseQuery.Analyzer = repoIndexerAnalyzer - keywordQuery = phraseQuery + phraseQuery := bleve.NewMatchPhraseQuery(opts.Keyword) + phraseQuery.FieldVal = "Content" + phraseQuery.Analyzer = repoIndexerAnalyzer + keywordQuery = phraseQuery + if opts.IsKeywordFuzzy { + phraseQuery.Fuzziness = len(opts.Keyword) / fuzzyDenominator } - if len(repoIDs) > 0 { - repoQueries := make([]query.Query, 0, len(repoIDs)) - for _, repoID := range repoIDs { + if len(opts.RepoIDs) > 0 { + repoQueries := make([]query.Query, 0, len(opts.RepoIDs)) + for _, repoID := range opts.RepoIDs { repoQueries = append(repoQueries, inner_bleve.NumericEqualityQuery(repoID, "RepoID")) } @@ -266,8 +265,8 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword // Save for reuse without language filter facetQuery := indexerQuery - if len(language) > 0 { - languageQuery := bleve.NewMatchQuery(language) + if len(opts.Language) > 0 { + languageQuery := bleve.NewMatchQuery(opts.Language) languageQuery.FieldVal = "Language" languageQuery.Analyzer = analyzer_keyword.Name @@ -277,12 +276,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword ) } - from := (page - 1) * pageSize + from, pageSize := opts.GetSkipTake() searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false) searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"} searchRequest.IncludeLocations = true - if len(language) == 0 { + if len(opts.Language) == 0 { searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10)) } @@ -326,7 +325,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword } searchResultLanguages := make([]*internal.SearchResultLanguages, 0, 10) - if len(language) > 0 { + if len(opts.Language) > 0 { // Use separate query to go get all language counts facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false) facetRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"} diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 0f70f13485..e4622fd66e 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -281,18 +281,18 @@ func extractAggs(searchResult *elastic.SearchResult) []*internal.SearchResultLan } // Search searches for codes and language stats by given conditions. -func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { - searchType := esMultiMatchTypeBestFields - if isMatch { - searchType = esMultiMatchTypePhrasePrefix +func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) { + searchType := esMultiMatchTypePhrasePrefix + if opts.IsKeywordFuzzy { + searchType = esMultiMatchTypeBestFields } - kwQuery := elastic.NewMultiMatchQuery(keyword, "content").Type(searchType) + kwQuery := elastic.NewMultiMatchQuery(opts.Keyword, "content").Type(searchType) query := elastic.NewBoolQuery() query = query.Must(kwQuery) - if len(repoIDs) > 0 { - repoStrs := make([]any, 0, len(repoIDs)) - for _, repoID := range repoIDs { + if len(opts.RepoIDs) > 0 { + repoStrs := make([]any, 0, len(opts.RepoIDs)) + for _, repoID := range opts.RepoIDs { repoStrs = append(repoStrs, repoID) } repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) @@ -300,16 +300,12 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword } var ( - start int - kw = "<em>" + keyword + "</em>" - aggregation = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc() + start, pageSize = opts.GetSkipTake() + kw = "<em>" + opts.Keyword + "</em>" + aggregation = elastic.NewTermsAggregation().Field("language").Size(10).OrderByCountDesc() ) - if page > 0 { - start = (page - 1) * pageSize - } - - if len(language) == 0 { + if len(opts.Language) == 0 { searchResult, err := b.inner.Client.Search(). Index(b.inner.VersionedIndexName()). Aggregation("language", aggregation). @@ -330,7 +326,7 @@ func (b *Indexer) Search(ctx context.Context, repoIDs []int64, language, keyword return convertResult(searchResult, kw, pageSize) } - langQuery := elastic.NewMatchQuery("language", language) + langQuery := elastic.NewMatchQuery("language", opts.Language) countResult, err := b.inner.Client.Search(). Index(b.inner.VersionedIndexName()). Aggregation("language", aggregation). diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index 942861e8c0..2905a540e5 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -10,7 +10,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -33,7 +32,7 @@ func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision s needGenesis := len(status.CommitSha) == 0 if !needGenesis { - hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision) + hasAncestorCmd := git.NewCommand(ctx, "merge-base").AddDynamicArguments(status.CommitSha, revision) stdout, _, _ := hasAncestorCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) needGenesis = len(stdout) == 0 } @@ -92,11 +91,9 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s return nil, runErr } + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + var err error - objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, repo) - if err != nil { - return nil, err - } changes.Updates, err = parseGitLsTreeOutput(objectFormat, stdout) return &changes, err } @@ -175,10 +172,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio return nil, err } - objectFormat, err := gitrepo.GetObjectFormatOfRepo(ctx, repo) - if err != nil { - return nil, err - } + objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) + changes.Updates, err = parseGitLsTreeOutput(objectFormat, lsTreeStdout) return &changes, err } diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 5eb8e61e3d..8975c5ce40 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -8,6 +8,7 @@ import ( "os" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/indexer/code/bleve" @@ -70,7 +71,15 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) { for _, kw := range keywords { t.Run(kw.Keyword, func(t *testing.T) { - total, res, langs, err := indexer.Search(context.TODO(), kw.RepoIDs, "", kw.Keyword, 1, 10, false) + total, res, langs, err := indexer.Search(context.TODO(), &internal.SearchOptions{ + RepoIDs: kw.RepoIDs, + Keyword: kw.Keyword, + Paginator: &db.ListOptions{ + Page: 1, + PageSize: 10, + }, + IsKeywordFuzzy: true, + }) assert.NoError(t, err) assert.Len(t, kw.IDs, int(total)) assert.Len(t, langs, kw.Langs) diff --git a/modules/indexer/code/internal/indexer.go b/modules/indexer/code/internal/indexer.go index da3ac3623c..c259fcd26e 100644 --- a/modules/indexer/code/internal/indexer.go +++ b/modules/indexer/code/internal/indexer.go @@ -7,6 +7,7 @@ import ( "context" "fmt" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/indexer/internal" ) @@ -16,7 +17,17 @@ type Indexer interface { internal.Indexer Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *RepoChanges) error Delete(ctx context.Context, repoID int64) error - Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) + Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error) +} + +type SearchOptions struct { + RepoIDs []int64 + Keyword string + Language string + + IsKeywordFuzzy bool + + db.Paginator } // NewDummyIndexer returns a dummy indexer @@ -38,6 +49,6 @@ func (d *dummyIndexer) Delete(ctx context.Context, repoID int64) error { return fmt.Errorf("indexer is not ready") } -func (d *dummyIndexer) Search(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int64, []*SearchResult, []*SearchResultLanguages, error) { +func (d *dummyIndexer) Search(ctx context.Context, opts *SearchOptions) (int64, []*SearchResult, []*SearchResultLanguages, error) { return 0, nil, nil, fmt.Errorf("indexer is not ready") } diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index e19e22eea0..74c957dde6 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -16,18 +16,24 @@ import ( // Result a search result to display type Result struct { - RepoID int64 - Filename string - CommitID string - UpdatedUnix timeutil.TimeStamp - Language string - Color string - LineNumbers []int - FormattedLines template.HTML + RepoID int64 + Filename string + CommitID string + UpdatedUnix timeutil.TimeStamp + Language string + Color string + Lines []*ResultLine +} + +type ResultLine struct { + Num int + FormattedContent template.HTML } type SearchResultLanguages = internal.SearchResultLanguages +type SearchOptions = internal.SearchOptions + func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { startIndex := selectionStartIndex numLinesBefore := 0 @@ -64,13 +70,29 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error { return nil } +func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine { + // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting + hl, _ := highlight.Code(filename, language, code) + highlightedLines := strings.Split(string(hl), "\n") + + // The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n` + lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums))) + for i := 0; i < len(lines); i++ { + lines[i] = &ResultLine{ + Num: lineNums[i], + FormattedContent: template.HTML(highlightedLines[i]), + } + } + return lines +} + func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Result, error) { startLineNum := 1 + strings.Count(result.Content[:startIndex], "\n") var formattedLinesBuffer bytes.Buffer contentLines := strings.SplitAfter(result.Content[startIndex:endIndex], "\n") - lineNumbers := make([]int, len(contentLines)) + lineNums := make([]int, 0, len(contentLines)) index := startIndex for i, line := range contentLines { var err error @@ -85,39 +107,35 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res line[closeActiveIndex:], ) } else { - err = writeStrings(&formattedLinesBuffer, - line, - ) + err = writeStrings(&formattedLinesBuffer, line) } if err != nil { return nil, err } - lineNumbers[i] = startLineNum + i + lineNums = append(lineNums, startLineNum+i) index += len(line) } - highlighted, _ := highlight.Code(result.Filename, "", formattedLinesBuffer.String()) - return &Result{ - RepoID: result.RepoID, - Filename: result.Filename, - CommitID: result.CommitID, - UpdatedUnix: result.UpdatedUnix, - Language: result.Language, - Color: result.Color, - LineNumbers: lineNumbers, - FormattedLines: highlighted, + RepoID: result.RepoID, + Filename: result.Filename, + CommitID: result.CommitID, + UpdatedUnix: result.UpdatedUnix, + Language: result.Language, + Color: result.Color, + Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()), }, nil } // PerformSearch perform a search on a repository -func PerformSearch(ctx context.Context, repoIDs []int64, language, keyword string, page, pageSize int, isMatch bool) (int, []*Result, []*internal.SearchResultLanguages, error) { - if len(keyword) == 0 { +// if isFuzzy is true set the Damerau-Levenshtein distance from 0 to 2 +func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) { + if opts == nil || len(opts.Keyword) == 0 { return 0, nil, nil, nil } - total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, repoIDs, language, keyword, page, pageSize, isMatch) + total, results, resultLanguages, err := (*globalIndexer.Load()).Search(ctx, opts) if err != nil { return 0, nil, nil, err } diff --git a/modules/indexer/internal/bleve/indexer.go b/modules/indexer/internal/bleve/indexer.go index ce06b5afcb..01e53ca636 100644 --- a/modules/indexer/internal/bleve/indexer.go +++ b/modules/indexer/internal/bleve/indexer.go @@ -92,7 +92,7 @@ func (i *Indexer) Ping(_ context.Context) error { } func (i *Indexer) Close() { - if i == nil { + if i == nil || i.Indexer == nil { return } diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go index c7d66538c1..21422b281c 100644 --- a/modules/indexer/internal/bleve/query.go +++ b/modules/indexer/internal/bleve/query.go @@ -4,6 +4,8 @@ package bleve import ( + "code.gitea.io/gitea/modules/optional" + "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/search/query" ) @@ -18,10 +20,11 @@ func NumericEqualityQuery(value int64, field string) *query.NumericRangeQuery { } // MatchPhraseQuery generates a match phrase query for the given phrase, field and analyzer -func MatchPhraseQuery(matchPhrase, field, analyzer string) *query.MatchPhraseQuery { +func MatchPhraseQuery(matchPhrase, field, analyzer string, fuzziness int) *query.MatchPhraseQuery { q := bleve.NewMatchPhraseQuery(matchPhrase) q.FieldVal = field q.Analyzer = analyzer + q.Fuzziness = fuzziness return q } @@ -32,18 +35,18 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery { return q } -func NumericRangeInclusiveQuery(min, max *int64, field string) *query.NumericRangeQuery { +func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery { var minF, maxF *float64 var minI, maxI *bool - if min != nil { + if min.Has() { minF = new(float64) - *minF = float64(*min) + *minF = float64(min.Value()) minI = new(bool) *minI = true } - if max != nil { + if max.Has() { maxF = new(float64) - *maxF = float64(*max) + *maxF = float64(max.Value()) maxI = new(bool) *maxI = true } diff --git a/modules/indexer/internal/meilisearch/indexer.go b/modules/indexer/internal/meilisearch/indexer.go index b037249d43..f4004849c1 100644 --- a/modules/indexer/internal/meilisearch/indexer.go +++ b/modules/indexer/internal/meilisearch/indexer.go @@ -87,8 +87,5 @@ func (i *Indexer) Close() { if i == nil { return } - if i.Client == nil { - return - } i.Client = nil } diff --git a/modules/indexer/internal/paginator.go b/modules/indexer/internal/paginator.go index de0a33c06f..ee204bf047 100644 --- a/modules/indexer/internal/paginator.go +++ b/modules/indexer/internal/paginator.go @@ -10,7 +10,7 @@ import ( ) // ParsePaginator parses a db.Paginator into a skip and limit -func ParsePaginator(paginator db.Paginator, max ...int) (int, int) { +func ParsePaginator(paginator *db.ListOptions, max ...int) (int, int) { // Use a very large number to indicate no limit unlimited := math.MaxInt32 if len(max) > 0 { @@ -19,22 +19,15 @@ func ParsePaginator(paginator db.Paginator, max ...int) (int, int) { } if paginator == nil || paginator.IsListAll() { + // It shouldn't happen. In actual usage scenarios, there should not be requests to search all. + // But if it does happen, respect it and return "unlimited". + // And it's also useful for testing. return 0, unlimited } - // Warning: Do not use GetSkipTake() for *db.ListOptions - // Its implementation could reset the page size with setting.API.MaxResponseItems - if listOptions, ok := paginator.(*db.ListOptions); ok { - if listOptions.Page >= 0 && listOptions.PageSize > 0 { - var start int - if listOptions.Page == 0 { - start = 0 - } else { - start = (listOptions.Page - 1) * listOptions.PageSize - } - return start, listOptions.PageSize - } - return 0, unlimited + if paginator.PageSize == 0 { + // Do not return any results when searching, it's used to get the total count only. + return 0, 0 } return paginator.GetSkipTake() diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 7c82cfbb79..1f54be721b 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -35,7 +35,11 @@ func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error { }) } -const maxBatchSize = 16 +const ( + maxBatchSize = 16 + // fuzzyDenominator determines the levenshtein distance per each character of a keyword + fuzzyDenominator = 4 +) // IndexerData an update to the issue indexer type IndexerData internal.IndexerData @@ -156,12 +160,16 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( var queries []query.Query if options.Keyword != "" { - keywordQueries := []query.Query{ - inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer), - inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer), - inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer), + fuzziness := 0 + if options.IsFuzzyKeyword { + fuzziness = len(options.Keyword) / fuzzyDenominator } - queries = append(queries, bleve.NewDisjunctionQuery(keywordQueries...)) + + queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{ + inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer, fuzziness), + inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer, fuzziness), + inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer, fuzziness), + }...)) } if len(options.RepoIDs) > 0 || options.AllPublic { @@ -175,11 +183,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...)) } - if !options.IsPull.IsNone() { - queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.IsTrue(), "is_pull")) + if options.IsPull.Has() { + queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull")) } - if !options.IsClosed.IsNone() { - queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.IsTrue(), "is_closed")) + if options.IsClosed.Has() { + queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed")) } if options.NoLabelOnly { @@ -217,38 +225,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...)) } - if options.ProjectID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectID, "project_id")) + if options.ProjectID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id")) } - if options.ProjectBoardID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectBoardID, "project_board_id")) + if options.ProjectBoardID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id")) } - if options.PosterID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.PosterID, "poster_id")) + if options.PosterID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id")) } - if options.AssigneeID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.AssigneeID, "assignee_id")) + if options.AssigneeID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id")) } - if options.MentionID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.MentionID, "mention_ids")) + if options.MentionID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.MentionID.Value(), "mention_ids")) } - if options.ReviewedID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewedID, "reviewed_ids")) + if options.ReviewedID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewedID.Value(), "reviewed_ids")) } - if options.ReviewRequestedID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewRequestedID, "review_requested_ids")) + if options.ReviewRequestedID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewRequestedID.Value(), "review_requested_ids")) } - if options.SubscriberID != nil { - queries = append(queries, inner_bleve.NumericEqualityQuery(*options.SubscriberID, "subscriber_ids")) + if options.SubscriberID.Has() { + queries = append(queries, inner_bleve.NumericEqualityQuery(options.SubscriberID.Value(), "subscriber_ids")) } - if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil { - queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(options.UpdatedAfterUnix, options.UpdatedBeforeUnix, "updated_unix")) + if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() { + queries = append(queries, inner_bleve.NumericRangeInclusiveQuery( + options.UpdatedAfterUnix, + options.UpdatedBeforeUnix, + "updated_unix")) } var indexerQuery query.Query = bleve.NewConjunctionQuery(queries...) diff --git a/modules/indexer/issues/db/db.go b/modules/indexer/issues/db/db.go index 1016523b72..05ec548435 100644 --- a/modules/indexer/issues/db/db.go +++ b/modules/indexer/issues/db/db.go @@ -78,6 +78,17 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( return nil, err } + // If pagesize == 0, return total count only. It's a special case for search count. + if options.Paginator != nil && options.Paginator.PageSize == 0 { + total, err := issue_model.CountIssues(ctx, opt, cond) + if err != nil { + return nil, err + } + return &internal.SearchResult{ + Total: total, + }, nil + } + ids, total, err := issue_model.IssueIDs(ctx, opt, cond) if err != nil { return nil, err diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 5406715bbc..eeaf1696ad 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -11,25 +11,10 @@ import ( issue_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/optional" ) func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) { - // See the comment of issues_model.SearchOptions for the reason why we need to convert - convertID := func(id *int64) int64 { - if id == nil { - return 0 - } - if *id == 0 { - return db.NoConditionID - } - return *id - } - convertInt64 := func(i *int64) int64 { - if i == nil { - return 0 - } - return *i - } var sortType string switch options.SortBy { case internal.SortByCreatedAsc: @@ -52,6 +37,18 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m sortType = "newest" } + // See the comment of issues_model.SearchOptions for the reason why we need to convert + convertID := func(id optional.Option[int64]) int64 { + if !id.Has() { + return 0 + } + value := id.Value() + if value == 0 { + return db.NoConditionID + } + return value + } + opts := &issue_model.IssuesOptions{ Paginator: options.Paginator, RepoIDs: options.RepoIDs, @@ -72,10 +69,10 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m IncludeMilestones: nil, SortType: sortType, IssueIDs: nil, - UpdatedAfterUnix: convertInt64(options.UpdatedAfterUnix), - UpdatedBeforeUnix: convertInt64(options.UpdatedBeforeUnix), + UpdatedAfterUnix: options.UpdatedAfterUnix.Value(), + UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(), PriorityRepoID: 0, - IsArchived: 0, + IsArchived: optional.None[bool](), Org: nil, Team: nil, User: nil, diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index 80e233e29a..4a98b4588a 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -6,6 +6,7 @@ package issues import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/optional" ) func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions { @@ -38,13 +39,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp } // See the comment of issues_model.SearchOptions for the reason why we need to convert - convertID := func(id int64) *int64 { + convertID := func(id int64) optional.Option[int64] { if id > 0 { - return &id + return optional.Some(id) } if id == db.NoConditionID { - var zero int64 - return &zero + return optional.None[int64]() } return nil } @@ -59,10 +59,10 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp searchOpt.SubscriberID = convertID(opts.SubscriberID) if opts.UpdatedAfterUnix > 0 { - searchOpt.UpdatedAfterUnix = &opts.UpdatedAfterUnix + searchOpt.UpdatedAfterUnix = optional.Some(opts.UpdatedAfterUnix) } if opts.UpdatedBeforeUnix > 0 { - searchOpt.UpdatedBeforeUnix = &opts.UpdatedBeforeUnix + searchOpt.UpdatedBeforeUnix = optional.Some(opts.UpdatedBeforeUnix) } searchOpt.Paginator = opts.Paginator diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index d059f76b32..53b383c8d5 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -19,6 +19,10 @@ import ( const ( issueIndexerLatestVersion = 1 + // multi-match-types, currently only 2 types are used + // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types + esMultiMatchTypeBestFields = "best_fields" + esMultiMatchTypePhrasePrefix = "phrase_prefix" ) var _ internal.Indexer = &Indexer{} @@ -141,7 +145,13 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query := elastic.NewBoolQuery() if options.Keyword != "" { - query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments")) + + searchType := esMultiMatchTypePhrasePrefix + if options.IsFuzzyKeyword { + searchType = esMultiMatchTypeBestFields + } + + query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(searchType)) } if len(options.RepoIDs) > 0 { @@ -153,11 +163,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.Must(q) } - if !options.IsPull.IsNone() { - query.Must(elastic.NewTermQuery("is_pull", options.IsPull.IsTrue())) + if options.IsPull.Has() { + query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value())) } - if !options.IsClosed.IsNone() { - query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.IsTrue())) + if options.IsClosed.Has() { + query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value())) } if options.NoLabelOnly { @@ -185,43 +195,43 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...)) } - if options.ProjectID != nil { - query.Must(elastic.NewTermQuery("project_id", *options.ProjectID)) + if options.ProjectID.Has() { + query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value())) } - if options.ProjectBoardID != nil { - query.Must(elastic.NewTermQuery("project_board_id", *options.ProjectBoardID)) + if options.ProjectBoardID.Has() { + query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value())) } - if options.PosterID != nil { - query.Must(elastic.NewTermQuery("poster_id", *options.PosterID)) + if options.PosterID.Has() { + query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value())) } - if options.AssigneeID != nil { - query.Must(elastic.NewTermQuery("assignee_id", *options.AssigneeID)) + if options.AssigneeID.Has() { + query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value())) } - if options.MentionID != nil { - query.Must(elastic.NewTermQuery("mention_ids", *options.MentionID)) + if options.MentionID.Has() { + query.Must(elastic.NewTermQuery("mention_ids", options.MentionID.Value())) } - if options.ReviewedID != nil { - query.Must(elastic.NewTermQuery("reviewed_ids", *options.ReviewedID)) + if options.ReviewedID.Has() { + query.Must(elastic.NewTermQuery("reviewed_ids", options.ReviewedID.Value())) } - if options.ReviewRequestedID != nil { - query.Must(elastic.NewTermQuery("review_requested_ids", *options.ReviewRequestedID)) + if options.ReviewRequestedID.Has() { + query.Must(elastic.NewTermQuery("review_requested_ids", options.ReviewRequestedID.Value())) } - if options.SubscriberID != nil { - query.Must(elastic.NewTermQuery("subscriber_ids", *options.SubscriberID)) + if options.SubscriberID.Has() { + query.Must(elastic.NewTermQuery("subscriber_ids", options.SubscriberID.Value())) } - if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil { + if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() { q := elastic.NewRangeQuery("updated_unix") - if options.UpdatedAfterUnix != nil { - q.Gte(*options.UpdatedAfterUnix) + if options.UpdatedAfterUnix.Has() { + q.Gte(options.UpdatedAfterUnix.Value()) } - if options.UpdatedBeforeUnix != nil { - q.Lte(*options.UpdatedBeforeUnix) + if options.UpdatedBeforeUnix.Has() { + q.Lte(options.UpdatedBeforeUnix.Value()) } query.Must(q) } diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 57037d2947..1cb86feb82 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -20,10 +20,10 @@ import ( "code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/indexer/issues/meilisearch" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) // IndexerMetadata is used to send data to the queue, so it contains only the ids. @@ -220,7 +220,7 @@ func PopulateIssueIndexer(ctx context.Context) error { ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, OrderBy: db_model.SearchOrderByID, Private: true, - Collaborate: util.OptionalBoolFalse, + Collaborate: optional.Some(false), }) if err != nil { log.Error("SearchRepositoryByName: %v", err) @@ -308,7 +308,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err // CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count. func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) { - opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} }) + opts = opts.Copy(func(options *SearchOptions) { options.Paginator = &db_model.ListOptions{PageSize: 0} }) _, total, err := SearchIssues(ctx, opts) return total, err diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index da4fc9b878..0d0cfc8516 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -10,8 +10,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" @@ -134,63 +134,60 @@ func searchIssueInRepo(t *testing.T) { } func searchIssueByID(t *testing.T) { - int64Pointer := func(x int64) *int64 { - return &x - } tests := []struct { opts SearchOptions expectedIDs []int64 }{ { - SearchOptions{ - PosterID: int64Pointer(1), + opts: SearchOptions{ + PosterID: optional.Some(int64(1)), }, - []int64{11, 6, 3, 2, 1}, + expectedIDs: []int64{11, 6, 3, 2, 1}, }, { - SearchOptions{ - AssigneeID: int64Pointer(1), + opts: SearchOptions{ + AssigneeID: optional.Some(int64(1)), }, - []int64{6, 1}, + expectedIDs: []int64{6, 1}, }, { - SearchOptions{ - MentionID: int64Pointer(4), + opts: SearchOptions{ + MentionID: optional.Some(int64(4)), }, - []int64{1}, + expectedIDs: []int64{1}, }, { - SearchOptions{ - ReviewedID: int64Pointer(1), + opts: SearchOptions{ + ReviewedID: optional.Some(int64(1)), }, - []int64{}, + expectedIDs: []int64{}, }, { - SearchOptions{ - ReviewRequestedID: int64Pointer(1), + opts: SearchOptions{ + ReviewRequestedID: optional.Some(int64(1)), }, - []int64{12}, + expectedIDs: []int64{12}, }, { - SearchOptions{ - SubscriberID: int64Pointer(1), + opts: SearchOptions{ + SubscriberID: optional.Some(int64(1)), }, - []int64{11, 6, 5, 3, 2, 1}, + expectedIDs: []int64{11, 6, 5, 3, 2, 1}, }, { // issue 20 request user 15 and team 5 which user 15 belongs to // the review request number of issue 20 should be 1 - SearchOptions{ - ReviewRequestedID: int64Pointer(15), + opts: SearchOptions{ + ReviewRequestedID: optional.Some(int64(15)), }, - []int64{12, 20}, + expectedIDs: []int64{12, 20}, }, { // user 20 approved the issue 20, so return nothing - SearchOptions{ - ReviewRequestedID: int64Pointer(20), + opts: SearchOptions{ + ReviewRequestedID: optional.Some(int64(20)), }, - []int64{}, + expectedIDs: []int64{}, }, } @@ -210,15 +207,15 @@ func searchIssueIsPull(t *testing.T) { }{ { SearchOptions{ - IsPull: util.OptionalBoolFalse, + IsPull: optional.Some(false), }, []int64{17, 16, 15, 14, 13, 6, 5, 18, 10, 7, 4, 1}, }, { SearchOptions{ - IsPull: util.OptionalBoolTrue, + IsPull: optional.Some(true), }, - []int64{12, 11, 20, 19, 9, 8, 3, 2}, + []int64{22, 21, 12, 11, 20, 19, 9, 8, 3, 2}, }, } for _, test := range tests { @@ -237,13 +234,13 @@ func searchIssueIsClosed(t *testing.T) { }{ { SearchOptions{ - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }, - []int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1}, + []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 19, 18, 10, 7, 9, 8, 3, 2, 1}, }, { SearchOptions{ - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }, []int64{5, 4}, }, @@ -305,7 +302,7 @@ func searchIssueByLabelID(t *testing.T) { SearchOptions{ ExcludedLabelIDs: []int64{1}, }, - []int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3}, + []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3}, }, } for _, test := range tests { @@ -318,18 +315,15 @@ func searchIssueByLabelID(t *testing.T) { } func searchIssueByTime(t *testing.T) { - int64Pointer := func(i int64) *int64 { - return &i - } tests := []struct { opts SearchOptions expectedIDs []int64 }{ { SearchOptions{ - UpdatedAfterUnix: int64Pointer(0), + UpdatedAfterUnix: optional.Some(int64(0)), }, - []int64{17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1}, + []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1}, }, } for _, test := range tests { @@ -350,7 +344,7 @@ func searchIssueWithOrder(t *testing.T) { SearchOptions{ SortBy: internal.SortByCreatedAsc, }, - []int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17}, + []int64{1, 2, 3, 8, 9, 4, 7, 10, 18, 19, 5, 6, 20, 11, 12, 13, 14, 15, 16, 17, 21, 22}, }, } for _, test := range tests { @@ -363,28 +357,25 @@ func searchIssueWithOrder(t *testing.T) { } func searchIssueInProject(t *testing.T) { - int64Pointer := func(i int64) *int64 { - return &i - } tests := []struct { opts SearchOptions expectedIDs []int64 }{ { SearchOptions{ - ProjectID: int64Pointer(1), + ProjectID: optional.Some(int64(1)), }, []int64{5, 3, 2, 1}, }, { SearchOptions{ - ProjectBoardID: int64Pointer(1), + ProjectBoardID: optional.Some(int64(1)), }, []int64{1}, }, { SearchOptions{ - ProjectBoardID: int64Pointer(0), // issue with in default board + ProjectBoardID: optional.Some(int64(0)), // issue with in default board }, []int64{2}, }, @@ -410,8 +401,8 @@ func searchIssueWithPaginator(t *testing.T) { PageSize: 5, }, }, - []int64{17, 16, 15, 14, 13}, - 20, + []int64{22, 21, 17, 16, 15}, + 22, }, } for _, test := range tests { diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 031745dd2f..e9c4eca559 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -5,8 +5,8 @@ package internal import ( "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) // IndexerData data stored in the issue indexer @@ -74,11 +74,13 @@ type SearchResult struct { type SearchOptions struct { Keyword string // keyword to search + IsFuzzyKeyword bool // if false the levenshtein distance is 0 + RepoIDs []int64 // repository IDs which the issues belong to AllPublic bool // if include all public repositories - IsPull util.OptionalBool // if the issues is a pull request - IsClosed util.OptionalBool // if the issues is closed + IsPull optional.Option[bool] // if the issues is a pull request + IsClosed optional.Option[bool] // if the issues is closed IncludedLabelIDs []int64 // labels the issues have ExcludedLabelIDs []int64 // labels the issues don't have @@ -87,24 +89,24 @@ type SearchOptions struct { MilestoneIDs []int64 // milestones the issues have - ProjectID *int64 // project the issues belong to - ProjectBoardID *int64 // project board the issues belong to + ProjectID optional.Option[int64] // project the issues belong to + ProjectBoardID optional.Option[int64] // project board the issues belong to - PosterID *int64 // poster of the issues + PosterID optional.Option[int64] // poster of the issues - AssigneeID *int64 // assignee of the issues, zero means no assignee + AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee - MentionID *int64 // mentioned user of the issues + MentionID optional.Option[int64] // mentioned user of the issues - ReviewedID *int64 // reviewer of the issues - ReviewRequestedID *int64 // requested reviewer of the issues + ReviewedID optional.Option[int64] // reviewer of the issues + ReviewRequestedID optional.Option[int64] // requested reviewer of the issues - SubscriberID *int64 // subscriber of the issues + SubscriberID optional.Option[int64] // subscriber of the issues - UpdatedAfterUnix *int64 - UpdatedBeforeUnix *int64 + UpdatedAfterUnix optional.Option[int64] + UpdatedBeforeUnix optional.Option[int64] - db.Paginator + Paginator *db.ListOptions SortBy SortBy // sort by field } diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 06fddeb65b..7f32876d80 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -16,8 +16,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/indexer/issues/internal" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -77,6 +77,13 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) { assert.Equal(t, c.ExpectedIDs, ids) assert.Equal(t, c.ExpectedTotal, result.Total) } + + // test counting + c.SearchOptions.Paginator = &db.ListOptions{PageSize: 0} + countResult, err := indexer.Search(context.Background(), c.SearchOptions) + require.NoError(t, err) + assert.Empty(t, countResult.Hits) + assert.Equal(t, result.Total, countResult.Total) }) } } @@ -166,7 +173,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsPull: util.OptionalBoolFalse, + IsPull: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -182,7 +189,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsPull: util.OptionalBoolTrue, + IsPull: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -198,7 +205,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsClosed: util.OptionalBoolFalse, + IsClosed: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -214,7 +221,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - IsClosed: util.OptionalBoolTrue, + IsClosed: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -300,10 +307,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectID: func() *int64 { - id := int64(1) - return &id - }(), + ProjectID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -321,10 +325,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectID: func() *int64 { - id := int64(0) - return &id - }(), + ProjectID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -342,10 +343,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectBoardID: func() *int64 { - id := int64(1) - return &id - }(), + ProjectBoardID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -363,10 +361,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ProjectBoardID: func() *int64 { - id := int64(0) - return &id - }(), + ProjectBoardID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -384,10 +379,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - PosterID: func() *int64 { - id := int64(1) - return &id - }(), + PosterID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -405,10 +397,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - AssigneeID: func() *int64 { - id := int64(1) - return &id - }(), + AssigneeID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -426,10 +415,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - AssigneeID: func() *int64 { - id := int64(0) - return &id - }(), + AssigneeID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -447,10 +433,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - MentionID: func() *int64 { - id := int64(1) - return &id - }(), + MentionID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -468,10 +451,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ReviewedID: func() *int64 { - id := int64(1) - return &id - }(), + ReviewedID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -489,10 +469,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - ReviewRequestedID: func() *int64 { - id := int64(1) - return &id - }(), + ReviewRequestedID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -510,10 +487,7 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - SubscriberID: func() *int64 { - id := int64(1) - return &id - }(), + SubscriberID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -531,14 +505,8 @@ var cases = []*testIndexerCase{ Paginator: &db.ListOptions{ PageSize: 5, }, - UpdatedAfterUnix: func() *int64 { - var t int64 = 20 - return &t - }(), - UpdatedBeforeUnix: func() *int64 { - var t int64 = 30 - return &t - }(), + UpdatedAfterUnix: optional.Some(int64(20)), + UpdatedBeforeUnix: optional.Some(int64(30)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, 5, len(result.Hits)) @@ -554,10 +522,8 @@ var cases = []*testIndexerCase{ { Name: "SortByCreatedDesc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByCreatedDesc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByCreatedDesc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) @@ -572,10 +538,8 @@ var cases = []*testIndexerCase{ { Name: "SortByUpdatedDesc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByUpdatedDesc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByUpdatedDesc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) @@ -590,10 +554,8 @@ var cases = []*testIndexerCase{ { Name: "SortByCommentsDesc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByCommentsDesc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByCommentsDesc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) @@ -608,10 +570,8 @@ var cases = []*testIndexerCase{ { Name: "SortByDeadlineDesc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByDeadlineDesc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByDeadlineDesc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) @@ -626,10 +586,8 @@ var cases = []*testIndexerCase{ { Name: "SortByCreatedAsc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByCreatedAsc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByCreatedAsc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) @@ -644,10 +602,8 @@ var cases = []*testIndexerCase{ { Name: "SortByUpdatedAsc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByUpdatedAsc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByUpdatedAsc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) @@ -662,10 +618,8 @@ var cases = []*testIndexerCase{ { Name: "SortByCommentsAsc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByCommentsAsc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByCommentsAsc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) @@ -680,10 +634,8 @@ var cases = []*testIndexerCase{ { Name: "SortByDeadlineAsc", SearchOptions: &internal.SearchOptions{ - Paginator: &db.ListOptions{ - ListAll: true, - }, - SortBy: internal.SortByDeadlineAsc, + Paginator: &db.ListOptionsAll, + SortBy: internal.SortByDeadlineAsc, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Equal(t, len(data), len(result.Hits)) diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index ab8dcd0af4..8a7cec6cba 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -5,6 +5,8 @@ package meilisearch import ( "context" + "errors" + "fmt" "strconv" "strings" @@ -16,12 +18,15 @@ import ( ) const ( - issueIndexerLatestVersion = 2 + issueIndexerLatestVersion = 3 // TODO: make this configurable if necessary maxTotalHits = 10000 ) +// ErrMalformedResponse is never expected as we initialize the indexer ourself and so define the types. +var ErrMalformedResponse = errors.New("meilisearch returned unexpected malformed content") + var _ internal.Indexer = &Indexer{} // Indexer implements Indexer interface @@ -47,6 +52,9 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { }, DisplayedAttributes: []string{ "id", + "title", + "content", + "comments", }, FilterableAttributes: []string{ "repo_id", @@ -131,11 +139,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(q) } - if !options.IsPull.IsNone() { - query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.IsTrue())) + if options.IsPull.Has() { + query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.Value())) } - if !options.IsClosed.IsNone() { - query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.IsTrue())) + if options.IsClosed.Has() { + query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value())) } if options.NoLabelOnly { @@ -163,41 +171,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...)) } - if options.ProjectID != nil { - query.And(inner_meilisearch.NewFilterEq("project_id", *options.ProjectID)) + if options.ProjectID.Has() { + query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value())) } - if options.ProjectBoardID != nil { - query.And(inner_meilisearch.NewFilterEq("project_board_id", *options.ProjectBoardID)) + if options.ProjectBoardID.Has() { + query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value())) } - if options.PosterID != nil { - query.And(inner_meilisearch.NewFilterEq("poster_id", *options.PosterID)) + if options.PosterID.Has() { + query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value())) } - if options.AssigneeID != nil { - query.And(inner_meilisearch.NewFilterEq("assignee_id", *options.AssigneeID)) + if options.AssigneeID.Has() { + query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value())) } - if options.MentionID != nil { - query.And(inner_meilisearch.NewFilterEq("mention_ids", *options.MentionID)) + if options.MentionID.Has() { + query.And(inner_meilisearch.NewFilterEq("mention_ids", options.MentionID.Value())) } - if options.ReviewedID != nil { - query.And(inner_meilisearch.NewFilterEq("reviewed_ids", *options.ReviewedID)) + if options.ReviewedID.Has() { + query.And(inner_meilisearch.NewFilterEq("reviewed_ids", options.ReviewedID.Value())) } - if options.ReviewRequestedID != nil { - query.And(inner_meilisearch.NewFilterEq("review_requested_ids", *options.ReviewRequestedID)) + if options.ReviewRequestedID.Has() { + query.And(inner_meilisearch.NewFilterEq("review_requested_ids", options.ReviewRequestedID.Value())) } - if options.SubscriberID != nil { - query.And(inner_meilisearch.NewFilterEq("subscriber_ids", *options.SubscriberID)) + if options.SubscriberID.Has() { + query.And(inner_meilisearch.NewFilterEq("subscriber_ids", options.SubscriberID.Value())) } - if options.UpdatedAfterUnix != nil { - query.And(inner_meilisearch.NewFilterGte("updated_unix", *options.UpdatedAfterUnix)) + if options.UpdatedAfterUnix.Has() { + query.And(inner_meilisearch.NewFilterGte("updated_unix", options.UpdatedAfterUnix.Value())) } - if options.UpdatedBeforeUnix != nil { - query.And(inner_meilisearch.NewFilterLte("updated_unix", *options.UpdatedBeforeUnix)) + if options.UpdatedBeforeUnix.Has() { + query.And(inner_meilisearch.NewFilterLte("updated_unix", options.UpdatedBeforeUnix.Value())) } if options.SortBy == "" { @@ -210,7 +218,22 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxTotalHits) - searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(options.Keyword, &meilisearch.SearchRequest{ + counting := limit == 0 + if counting { + // If set limit to 0, it will be 20 by default, and -1 is not allowed. + // See https://www.meilisearch.com/docs/reference/api/search#limit + // So set limit to 1 to make the cost as low as possible, then clear the result before returning. + limit = 1 + } + + keyword := options.Keyword + if !options.IsFuzzyKeyword { + // to make it non fuzzy ("typo tolerance" in meilisearch terms), we have to quote the keyword(s) + // https://www.meilisearch.com/docs/reference/api/search#phrase-search + keyword = doubleQuoteKeyword(keyword) + } + + searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{ Filter: query.Statement(), Limit: int64(limit), Offset: int64(skip), @@ -221,11 +244,13 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( return nil, err } - hits := make([]internal.Match, 0, len(searchRes.Hits)) - for _, hit := range searchRes.Hits { - hits = append(hits, internal.Match{ - ID: int64(hit.(map[string]any)["id"].(float64)), - }) + if counting { + searchRes.Hits = nil + } + + hits, err := convertHits(searchRes) + if err != nil { + return nil, err } return &internal.SearchResult{ @@ -241,3 +266,36 @@ func parseSortBy(sortBy internal.SortBy) string { } return field + ":asc" } + +func doubleQuoteKeyword(k string) string { + kp := strings.Split(k, " ") + parts := 0 + for i := range kp { + part := strings.Trim(kp[i], "\"") + if part != "" { + kp[parts] = fmt.Sprintf(`"%s"`, part) + parts++ + } + } + return strings.Join(kp[:parts], " ") +} + +func convertHits(searchRes *meilisearch.SearchResponse) ([]internal.Match, error) { + hits := make([]internal.Match, 0, len(searchRes.Hits)) + for _, hit := range searchRes.Hits { + hit, ok := hit.(map[string]any) + if !ok { + return nil, ErrMalformedResponse + } + + issueID, ok := hit["id"].(float64) + if !ok { + return nil, ErrMalformedResponse + } + + hits = append(hits, internal.Match{ + ID: int64(issueID), + }) + } + return hits, nil +} diff --git a/modules/indexer/issues/meilisearch/meilisearch_test.go b/modules/indexer/issues/meilisearch/meilisearch_test.go index 3d7237268e..4666df136a 100644 --- a/modules/indexer/issues/meilisearch/meilisearch_test.go +++ b/modules/indexer/issues/meilisearch/meilisearch_test.go @@ -10,7 +10,11 @@ import ( "testing" "time" + "code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/indexer/issues/internal/tests" + + "github.com/meilisearch/meilisearch-go" + "github.com/stretchr/testify/assert" ) func TestMeilisearchIndexer(t *testing.T) { @@ -48,3 +52,44 @@ func TestMeilisearchIndexer(t *testing.T) { tests.TestIndexer(t, indexer) } + +func TestConvertHits(t *testing.T) { + _, err := convertHits(&meilisearch.SearchResponse{ + Hits: []any{"aa", "bb", "cc", "dd"}, + }) + assert.ErrorIs(t, err, ErrMalformedResponse) + + validResponse := &meilisearch.SearchResponse{ + Hits: []any{ + map[string]any{ + "id": float64(11), + "title": "a title", + "content": "issue body with no match", + "comments": []any{"hey whats up?", "I'm currently bowling", "nice"}, + }, + map[string]any{ + "id": float64(22), + "title": "Bowling as title", + "content": "", + "comments": []any{}, + }, + map[string]any{ + "id": float64(33), + "title": "Bowl-ing as fuzzy match", + "content": "", + "comments": []any{}, + }, + }, + } + hits, err := convertHits(validResponse) + assert.NoError(t, err) + assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits) +} + +func TestDoubleQuoteKeyword(t *testing.T) { + assert.EqualValues(t, "", doubleQuoteKeyword("")) + assert.EqualValues(t, `"a" "b" "c"`, doubleQuoteKeyword("a b c")) + assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a d g")) + assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a d g")) + assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword(`a "" "d" """g`)) +} diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 510b4060b2..9861c808dc 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -61,9 +61,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD ) { reviews, err := issue_model.FindReviews(ctx, issue_model.FindReviewOptions{ - ListOptions: db.ListOptions{ - ListAll: true, - }, + ListOptions: db.ListOptionsAll, IssueID: issueID, OfficialOnly: false, }) diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 4e813fc91f..3be48b9edc 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -122,7 +122,13 @@ func validateRequired(field *api.IssueFormField, idx int) error { // The label is not required for a markdown or checkboxes field return nil } - return validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required") + if err := validateBoolItem(newErrorPosition(idx, field.Type), field.Validations, "required"); err != nil { + return err + } + if required, _ := field.Validations["required"].(bool); required && !field.VisibleOnForm() { + return newErrorPosition(idx, field.Type).Errorf("can not require a hidden field") + } + return nil } func validateID(field *api.IssueFormField, idx int, ids container.Set[string]) error { @@ -172,10 +178,38 @@ func validateOptions(field *api.IssueFormField, idx int) error { return position.Errorf("'label' is required and should be a string") } + if visibility, ok := opt["visible"]; ok { + visibilityList, ok := visibility.([]any) + if !ok { + return position.Errorf("'visible' should be list") + } + for _, visibleType := range visibilityList { + visibleType, ok := visibleType.(string) + if !ok || !(visibleType == "form" || visibleType == "content") { + return position.Errorf("'visible' list can only contain strings of 'form' and 'content'") + } + } + } + if required, ok := opt["required"]; ok { if _, ok := required.(bool); !ok { return position.Errorf("'required' should be a bool") } + + // validate if hidden field is required + if visibility, ok := opt["visible"]; ok { + visibilityList, _ := visibility.([]any) + isVisible := false + for _, v := range visibilityList { + if vv, _ := v.(string); vv == "form" { + isVisible = true + break + } + } + if !isVisible { + return position.Errorf("can not require a hidden checkbox") + } + } } } } @@ -238,7 +272,7 @@ func RenderToMarkdown(template *api.IssueTemplate, values url.Values) string { IssueFormField: field, Values: values, } - if f.ID == "" { + if f.ID == "" || !f.VisibleInContent() { continue } f.WriteTo(builder) @@ -253,11 +287,6 @@ type valuedField struct { } func (f *valuedField) WriteTo(builder *strings.Builder) { - if f.Type == api.IssueFormFieldTypeMarkdown { - // markdown blocks do not appear in output - return - } - // write label if !f.HideLabel() { _, _ = fmt.Fprintf(builder, "### %s\n\n", f.Label()) @@ -269,6 +298,9 @@ func (f *valuedField) WriteTo(builder *strings.Builder) { switch f.Type { case api.IssueFormFieldTypeCheckboxes: for _, option := range f.Options() { + if !option.VisibleInContent() { + continue + } checked := " " if option.IsChecked() { checked = "x" @@ -302,6 +334,10 @@ func (f *valuedField) WriteTo(builder *strings.Builder) { } else { _, _ = fmt.Fprintf(builder, "%s\n", value) } + case api.IssueFormFieldTypeMarkdown: + if value, ok := f.Attributes["value"].(string); ok { + _, _ = fmt.Fprintf(builder, "%s\n", value) + } } _, _ = fmt.Fprintln(builder) } @@ -314,6 +350,9 @@ func (f *valuedField) Label() string { } func (f *valuedField) HideLabel() bool { + if f.Type == api.IssueFormFieldTypeMarkdown { + return true + } if label, ok := f.Attributes["hide_label"].(bool); ok { return label } @@ -385,6 +424,22 @@ func (o *valuedOption) IsChecked() bool { return false } +func (o *valuedOption) VisibleInContent() bool { + if o.field.Type == api.IssueFormFieldTypeCheckboxes { + if vs, ok := o.data.(map[string]any); ok { + if vl, ok := vs["visible"].([]any); ok { + for _, v := range vl { + if vv, _ := v.(string); vv == "content" { + return true + } + } + return false + } + } + } + return true +} + var minQuotesRegex = regexp.MustCompilePOSIX("^`{3,}") // minQuotes return 3 or more back-quotes. diff --git a/modules/issue/template/template_test.go b/modules/issue/template/template_test.go index 06e6b70d35..e24b962d61 100644 --- a/modules/issue/template/template_test.go +++ b/modules/issue/template/template_test.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -318,6 +319,42 @@ body: `, wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool", }, + { + name: "field is required but hidden", + content: ` +name: "test" +about: "this is about" +body: + - type: "input" + id: "1" + attributes: + label: "a" + validations: + required: true + visible: [content] +`, + wantErr: "body[0](input): can not require a hidden field", + }, + { + name: "checkboxes is required but hidden", + content: ` +name: "test" +about: "this is about" +body: + - type: checkboxes + id: "1" + attributes: + label: Label of checkboxes + description: Description of checkboxes + options: + - label: Option 1 + required: false + - label: Required and hidden + required: true + visible: [content] +`, + wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox", + }, { name: "valid", content: ` @@ -374,8 +411,11 @@ body: required: true - label: Option 2 of checkboxes required: false - - label: Option 3 of checkboxes + - label: Hidden Option 3 of checkboxes + visible: [content] + - label: Required but not submitted required: true + visible: [form] `, want: &api.IssueTemplate{ Name: "Name", @@ -390,6 +430,7 @@ body: Attributes: map[string]any{ "value": "Value of the markdown", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, }, { Type: "textarea", @@ -404,6 +445,7 @@ body: Validations: map[string]any{ "required": true, }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, { Type: "input", @@ -419,6 +461,7 @@ body: "is_number": true, "regex": "[a-zA-Z0-9]+", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, { Type: "dropdown", @@ -436,6 +479,7 @@ body: Validations: map[string]any{ "required": true, }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, { Type: "checkboxes", @@ -446,9 +490,11 @@ body: "options": []any{ map[string]any{"label": "Option 1 of checkboxes", "required": true}, map[string]any{"label": "Option 2 of checkboxes", "required": false}, - map[string]any{"label": "Option 3 of checkboxes", "required": true}, + map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}}, + map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}}, }, }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, }, }, FileName: "test.yaml", @@ -467,7 +513,12 @@ body: - type: markdown id: id1 attributes: - value: Value of the markdown + value: Value of the markdown shown in form + - type: markdown + id: id2 + attributes: + value: Value of the markdown shown in created issue + visible: [content] `, want: &api.IssueTemplate{ Name: "Name", @@ -480,8 +531,17 @@ body: Type: "markdown", ID: "id1", Attributes: map[string]any{ - "value": "Value of the markdown", + "value": "Value of the markdown shown in form", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, + }, + { + Type: "markdown", + ID: "id2", + Attributes: map[string]any{ + "value": "Value of the markdown shown in created issue", + }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent}, }, }, FileName: "test.yaml", @@ -515,6 +575,7 @@ body: Attributes: map[string]any{ "value": "Value of the markdown", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, }, }, FileName: "test.yaml", @@ -548,6 +609,7 @@ body: Attributes: map[string]any{ "value": "Value of the markdown", }, + Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, }, }, FileName: "test.yaml", @@ -622,9 +684,14 @@ body: - type: markdown id: id1 attributes: - value: Value of the markdown - - type: textarea + value: Value of the markdown shown in form + - type: markdown id: id2 + attributes: + value: Value of the markdown shown in created issue + visible: [content] + - type: textarea + id: id3 attributes: label: Label of textarea description: Description of textarea @@ -634,7 +701,7 @@ body: validations: required: true - type: input - id: id3 + id: id4 attributes: label: Label of input description: Description of input @@ -646,7 +713,7 @@ body: is_number: true regex: "[a-zA-Z0-9]+" - type: dropdown - id: id4 + id: id5 attributes: label: Label of dropdown description: Description of dropdown @@ -658,7 +725,7 @@ body: validations: required: true - type: checkboxes - id: id5 + id: id6 attributes: label: Label of checkboxes description: Description of checkboxes @@ -669,20 +736,26 @@ body: required: false - label: Option 3 of checkboxes required: true + visible: [form] + - label: Hidden Option of checkboxes + visible: [content] `, values: map[string][]string{ - "form-field-id2": {"Value of id2"}, "form-field-id3": {"Value of id3"}, - "form-field-id4": {"0,1"}, - "form-field-id5-0": {"on"}, - "form-field-id5-2": {"on"}, + "form-field-id4": {"Value of id4"}, + "form-field-id5": {"0,1"}, + "form-field-id6-0": {"on"}, + "form-field-id6-2": {"on"}, }, }, - want: `### Label of textarea -` + "```bash\nValue of id2\n```" + ` + want: `Value of the markdown shown in created issue -Value of id3 +### Label of textarea + +` + "```bash\nValue of id3\n```" + ` + +Value of id4 ### Label of dropdown @@ -692,7 +765,7 @@ Option 1 of dropdown, Option 2 of dropdown - [x] Option 1 of checkboxes - [ ] Option 2 of checkboxes -- [x] Option 3 of checkboxes +- [ ] Hidden Option of checkboxes `, }, @@ -704,7 +777,7 @@ Option 1 of dropdown, Option 2 of dropdown t.Fatal(err) } if got := RenderToMarkdown(template, tt.args.values); got != tt.want { - t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want) + assert.EqualValues(t, tt.want, got) } }) } diff --git a/modules/issue/template/unmarshal.go b/modules/issue/template/unmarshal.go index 8cae8d4c42..0fc13d7ddf 100644 --- a/modules/issue/template/unmarshal.go +++ b/modules/issue/template/unmarshal.go @@ -128,9 +128,18 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) { } } for i, v := range it.Fields { + // set default id value if v.ID == "" { v.ID = strconv.Itoa(i) } + // set default visibility + if v.Visible == nil { + v.Visible = []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm} + // markdown is not submitted by default + if v.Type != api.IssueFormFieldTypeMarkdown { + v.Visible = append(v.Visible, api.IssueFormFieldVisibleContent) + } + } } } diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go index daf8c6cfdd..0d9c0c98ac 100644 --- a/modules/lfs/content_store.go +++ b/modules/lfs/content_store.go @@ -4,6 +4,7 @@ package lfs import ( + "crypto/sha256" "encoding/hex" "errors" "hash" @@ -12,8 +13,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" - - "github.com/minio/sha256-simd" ) var ( diff --git a/modules/lfs/filesystem_client.go b/modules/lfs/filesystem_client.go index 3503a9effc..71bef5c899 100644 --- a/modules/lfs/filesystem_client.go +++ b/modules/lfs/filesystem_client.go @@ -44,7 +44,7 @@ func (c *FilesystemClient) Download(ctx context.Context, objects []Pointer, call if err != nil { return err } - + defer f.Close() if err := callback(p, f, nil); err != nil { return err } @@ -75,7 +75,7 @@ func (c *FilesystemClient) Upload(ctx context.Context, objects []Pointer, callba if err != nil { return err } - + defer f.Close() _, err = io.Copy(f, content) return err diff --git a/modules/lfs/pointer.go b/modules/lfs/pointer.go index 3e5bb8f91d..ebde20f826 100644 --- a/modules/lfs/pointer.go +++ b/modules/lfs/pointer.go @@ -4,6 +4,7 @@ package lfs import ( + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -12,8 +13,6 @@ import ( "regexp" "strconv" "strings" - - "github.com/minio/sha256-simd" ) const ( diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go index 7af34a6cbc..1dd26eb8ac 100644 --- a/modules/markup/csv/csv.go +++ b/modules/markup/csv/csv.go @@ -6,6 +6,7 @@ package markup import ( "bufio" "bytes" + "fmt" "html" "io" "regexp" @@ -77,27 +78,65 @@ func writeField(w io.Writer, element, class, field string) error { } // Render implements markup.Renderer -func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { +func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { tmpBlock := bufio.NewWriter(output) + maxSize := setting.UI.CSV.MaxFileSize - // FIXME: don't read all to memory - rawBytes, err := io.ReadAll(input) + if maxSize == 0 { + return r.tableRender(ctx, input, tmpBlock) + } + + rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1)) if err != nil { return err } - if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < int64(len(rawBytes)) { - if _, err := tmpBlock.WriteString("<pre>"); err != nil { - return err - } - if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil { - return err - } - _, err = tmpBlock.WriteString("</pre>") + 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("<pre>") + 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("</pre>") + 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,<a>\n2,<b>"), bufio.NewWriter(&buf)) + assert.NoError(t, err) + want := "<pre>1,<a>\n2,<b></pre>" + 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 "<p>{TextBefore}<div NewNode/>{TextAfter}</p>" (the parent could also be "li") + // then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>", + // 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 "<div>code preview</div>", nil + }, + }) + test := func(input, expected string) { + buffer, err := markup.RenderString(&markup.RenderContext{ + Ctx: git.DefaultContext, + Type: "markdown", + }, input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>") + test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`) +} 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", `<p><a href="magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download" rel="nofollow">magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download</a></p>`) + test( + `[link](https://example.com)`, + `<p><a href="https://example.com" rel="nofollow">link</a></p>`) + test( + `[link](mailto:test@example.com)`, + `<p><a href="mailto:test@example.com" rel="nofollow">link</a></p>`) + test( + `[link](javascript:xss)`, + `<p>link</p>`) // 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?]]", `<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a> <a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`, `<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a> <a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`) + test( + "[[markdown_file.md]]", + `<p><a href="`+renderableFileURL+`" rel="nofollow">markdown_file.md</a></p>`, + `<p><a href="`+renderableFileURLWiki+`" rel="nofollow">markdown_file.md</a></p>`) + test( + "[[file.zip]]", + `<p><a href="`+unrenderableFileURL+`" rel="nofollow">file.zip</a></p>`, + `<p><a href="`+unrenderableFileURLWiki+`" rel="nofollow">file.zip</a></p>`) test( "[[Link #.jpg]]", `<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`, @@ -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, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", 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("<code") - html.RenderAttributes(w, n, html.CodeAttributeFilter) - _ = w.WriteByte('>') - } else { - _, _ = w.WriteString("<code>") - } - 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(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color))) - } - } - return ast.WalkSkipChildren, nil - } - _, _ = w.WriteString("</code>") - 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(`<span class="attention-icon attention-`) - n := node.(*Attention) - _, _ = w.WriteString(strings.ToLower(n.AttentionType)) - _, _ = w.WriteString(`">`) - - var octiconType string - switch n.AttentionType { - case AttentionNote: - octiconType = "info" - case AttentionWarning: - octiconType = "alert" - } - _, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType))) - } else { - _, _ = w.WriteString("</span>\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("<li") - html.RenderAttributes(w, n, html.ListItemAttributeFilter) - _ = w.WriteByte('>') - } else { - _, _ = w.WriteString("<li>") - } - fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition) - if n.IsChecked { - _, _ = w.WriteString(` checked=""`) - } - if r.XHTML { - _, _ = w.WriteString(` />`) - } else { - _ = w.WriteByte('>') - } - fc := n.FirstChild() - if fc != nil { - if _, ok := fc.(*ast.TextBlock); !ok { - _ = w.WriteByte('\n') - } - } - } else { - _, _ = w.WriteString("</li>\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(`<code class="chroma language-` + string(language) + `">`) + // the "display" class is used by "js/markup/math.js" to render the code element as a block + _, err = w.WriteString(`<code class="chroma language-` + string(language) + ` display">`) 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 := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` test("<https://google.com/>", 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)", `<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`, `<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`) @@ -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 { <li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li> <li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li> </ul> -<p>See commit <a href="http://localhost:3000/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p> +<p>See commit <a href="/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p> <p>Ideas and codes</p> <ul> -<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li> -<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li> +<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li> +<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li> <li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li> <li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li> <li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li> @@ -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 - `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> + `<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> `, // special syntax `[[Name|Link]]`, // rendered - `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p> + `<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p> `, } @@ -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", + "<p>The CSS class <code>red</code> is there</p>\n", + }, { // hex "`#FF0000`", `<p><code>#FF0000<span class="color-preview" style="background-color: #FF0000"></span></code></p>` + nl, @@ -446,8 +449,8 @@ func TestColorPreview(t *testing.T) { `<p><code>rgb(16, 32, 64)<span class="color-preview" style="background-color: rgb(16, 32, 64)"></span></code></p>` + nl, }, { // short hex - "This is the color white `#000`", - `<p>This is the color white <code>#000<span class="color-preview" style="background-color: #000"></span></code></p>` + nl, + "This is the color white `#0a0`", + `<p>This is the color white <code>#0a0<span class="color-preview" style="background-color: #0a0"></span></code></p>` + 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\)`, `<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl, }, + { + `$a$.`, + `<p><code class="language-math is-loading">a</code>.</p>` + nl, + }, + { + `.$a$`, + `<p>.$a$</p>` + nl, + }, { `$a a$b b$`, - `<p><code class="language-math is-loading">a a$b b</code></p>` + nl, + `<p>$a a$b b$</p>` + nl, }, { `a a$b b`, @@ -518,7 +529,15 @@ func TestMathBlock(t *testing.T) { }, { `a$b $a a$b b$`, - `<p>a$b <code class="language-math is-loading">a a$b b</code></p>` + nl, + `<p>a$b $a a$b b$</p>` + nl, + }, + { + "a$x$", + `<p>a$x$</p>` + nl, + }, + { + "$x$a", + `<p>$x$a</p>` + 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</p> Expected: `<p>space @mention-user<br/> /just/a/path.bin<br/> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/> -<a href="/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> -<a href="/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/> <a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/> @@ -691,9 +711,9 @@ space</p> Expected: `<p>space @mention-user<br/> /just/a/path.bin<br/> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/> -<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> -<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="https://gitea.io/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/> <a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/> @@ -749,9 +769,9 @@ space</p> Expected: `<p>space @mention-user<br/> /just/a/path.bin<br/> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/> @@ -809,9 +829,9 @@ space</p> Expected: `<p>space @mention-user<br/> /just/a/path.bin<br/> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/> @@ -869,9 +889,9 @@ space</p> Expected: `<p>space @mention-user<br/> /just/a/path.bin<br/> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/> @@ -931,9 +951,9 @@ space</p> Expected: `<p>space @mention-user<br/> /just/a/path.bin<br/> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> -<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> +<a href="/relative/path/wiki/raw/file.bin" rel="nofollow">local link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/> @@ -957,6 +977,39 @@ space</p> 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 := `<blockquote class="attention-header attention-{attention}"><p><svg class="attention-icon attention-{attention} svg {icon}" width="16" height="16"></svg><strong class="attention-{attention}">{Attention}</strong></p>` + 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")+"\n<p>text</p>\n</blockquote>") + + test(`> [!note]`, renderAttention("note", "octicon-info")+"\n</blockquote>") + test(`> [!tip]`, renderAttention("tip", "octicon-light-bulb")+"\n</blockquote>") + test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>") + test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>") + test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\n</blockquote>") +} 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("<code") + html.RenderAttributes(w, n, html.CodeAttributeFilter) + _ = w.WriteByte('>') + } else { + _, _ = w.WriteString("<code>") + } + 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(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color))) + } + } + return ast.WalkSkipChildren, nil + } + _, _ = w.WriteString("</code>") + 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("<li") + html.RenderAttributes(w, n, html.ListItemAttributeFilter) + _ = w.WriteByte('>') + } else { + _, _ = w.WriteString("<li>") + } + fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition) + if n.IsChecked { + _, _ = w.WriteString(` checked=""`) + } + if r.XHTML { + _, _ = w.WriteString(` />`) + } else { + _ = w.WriteByte('>') + } + fc := n.FirstChild() + if fc != nil { + if _, ok := fc.(*ast.TextBlock); !ok { + _ = w.WriteByte('\n') + } + } + } else { + _, _ = w.WriteString("</li>\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/]]", + `<p><a href="https://google.com/">https://google.com/</a></p>`, false) + test("[[WikiPage][The WikiPage Desc]]", + `<p><a href="/relative-path/wiki/WikiPage">The WikiPage Desc</a></p>`, true) + test("[[ImageLink.svg][The Image Desc]]", + `<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`, 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/]]", - `<p><a href="https://google.com/">https://google.com/</a></p>`) - test("[[WikiPage][The WikiPage Desc]]", - `<p><a href="/relative-path/WikiPage">The WikiPage Desc</a></p>`) - test("[[ImageLink.svg][The Image Desc]]", - `<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`) + test("[[file:test.org][Test]]", + `<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`) + test("[[./test.org][Test]]", + `<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`) + test("[[test.org][Test]]", + `<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`) + test("[[path/to/test.org][Test]]", + `<p><a href="/relative-path/src/branch/main/path/to/test.org">Test</a></p>`) } 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 = `<?xml version="1.0" encoding="utf-8"?> <description>` + description + `</description> <releaseNotes>` + releaseNotes + `</releaseNotes> <repository url="` + repositoryURL + `" /> + <readme>README.md</readme> <dependencies> <group targetFramework="` + targetFramework + `"> <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" /> @@ -60,17 +61,19 @@ const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?> </package>` 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", `<?xml version="1.0" encoding="utf-8"?> + data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <metadata></metadata> - </package>`) + </package>`}) 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", `<?xml version="1.0" encoding="utf-8"?> + data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <metadata> - <id>`+id+`</id> + <id>` + id + `</id> </metadata> - </package>`) + </package>`}) 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(`<?xml version="1.0" encoding="utf-8"?> -<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> - <metadata> - <id>test</id> - <version>1.04.5.2.5-rc.1+metadata</version> - </metadata> -</package>`)) + data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?> + <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata> + <id>test</id> + <version>1.04.5.2.5-rc.1+metadata</version> + </metadata> + </package>`}) + + 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 <remote> +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err) - return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add <remote> +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(`<svg class="svg %s" width="%d" height="%d"></svg>`, 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("<span>%s(%d/%s)</span>", 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("<a>< < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1)) +} + +func TestSanitizeHTML(t *testing.T) { + assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`)) +} 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("<div class='ui label' style='color: %s !important; background-color: %s !important' data-tooltip-content title='%s'>%s</div>", - textColor, label.Color, description, RenderEmoji(ctx, label.Name)) + s := fmt.Sprintf("<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>", + 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("<span class='ui label scope-parent' data-tooltip-content title='%s'>"+ + s := fmt.Sprintf("<span class='ui label %s scope-parent' data-tooltip-content title='%s'>"+ "<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+ "<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+ "</span>", - 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 := `<span class="labels-list">` 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("<a href='%s/issues?labels=%d'>%s</a> ", - repoLink, label.ID, RenderLabel(ctx, label)) + repoLink, label.ID, RenderLabel(ctx, locale, label)) } htmlCode += "</span>" 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 <span class="emoji" aria-label="thumbs up">👍</span> <a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a> -<a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> test -<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a> +<a href="/mention-user" class="mention">@mention-user</a> test +<a href="/user13/repo11/issues/123" class="ref-issue">#123</a> space` assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas)) } func TestRenderCommitMessage(t *testing.T) { - expected := `space <a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> ` + expected := `space <a href="/mention-user" class="mention">@mention-user</a> ` assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas)) } func TestRenderCommitMessageLinkSubject(t *testing.T) { - expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>` + expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" class="mention">@mention-user</a>` assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas)) } @@ -155,14 +155,14 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit <span class="emoji" aria-label="thumbs up">👍</span> mail@domain.com @mention-user test -<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a> +<a href="/user13/repo11/issues/123" class="ref-issue">#123</a> space ` assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas)) } func TestRenderMarkdownToHtml(t *testing.T) { - expected := `<p>space <a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a><br/> + expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/> /just/a/path.bin <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a> <a href="/file.bin" rel="nofollow">local link</a> @@ -179,7 +179,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582 com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit <span class="emoji" aria-label="thumbs up">👍</span> <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a> -<a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a> test +<a href="/mention-user" rel="nofollow">@mention-user</a> test #123 space</p> ` 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(`<absolute-date %s date="%s">%s</absolute-date>`, 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(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped)) default: panic(fmt.Sprintf("Unsupported format %s", format)) } - return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, 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, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="invalid">invalid</relative-time>`, actual) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="invalid">invalid</absolute-date>`, actual) actual = DateTime("short", refTimeStr) - assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</relative-time>`, actual) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</absolute-date>`, actual) actual = DateTime("short", refTime) - assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01</relative-time>`, actual) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</absolute-date>`, actual) + + actual = DateTime("short", refDateStr) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01">2018-01-01</absolute-date>`, actual) actual = DateTime("short", refTimeStamp) - assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31</relative-time>`, actual) + assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</absolute-date>`, actual) actual = DateTime("full", refTimeStamp) - assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual) + assert.EqualValues(t, `<relative-time weekday="" year="numeric" format="datetime" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" data-tooltip-content data-tooltip-interactive="true" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, 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(`<relative-time class="time-since" prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`, + htm := fmt.Sprintf(`<relative-time prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`, 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; <span style="color: red\; background: none;">more text</span> +mixed = test value; <span style="color: red\; background: none;">%s</span> `) 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; <span style="color: red; background: none;">more text</span>`, result) + result2 := lang2.TrHTML("section.mixed", "a&b") + assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&b</span>`, 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=<a>%s</a>`), 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>", "<a><str></a>"}, + {[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"}, + {template.HTML("<html>"), "<a><html></a>"}, + {stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{<stringerPointerReceiver>}</a>"}, + {&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a><stringerPointerReceiver ptr></a>"}, + {stringerStructReceiver{"<stringerStructReceiver>"}, "<a><stringerStructReceiver></a>"}, + {&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a><stringerStructReceiver ptr></a>"}, + {errorStructReceiver{"<errorStructReceiver>"}, "<a><errorStructReceiver></a>"}, + {&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a><errorStructReceiver ptr></a>"}, + {errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{<errorPointerReceiver>}</a>"}, + {&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a><errorPointerReceiver ptr></a>"}, + } + + 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 <lf@elemental.net> + +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 + <paulus@ozlabs.org>". + +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 <solar at openwall.com> 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ď <strong>neexistuje</strong>, nebo <strong>nemáte oprávnění</strong> 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 <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> 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 <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">spusťte jako binární program</a> pro vaši platformu, nasaďte jej pomocí <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, nebo jej stáhněte jako <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">balíček</a>. platform=Multiplatformní platform_desc=Gitea běží všude, kde <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> 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"<email@example.com>. 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, <a href="%s">zobrazit nefiltrované výsledky</a>. @@ -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 <b>%s</b> 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 <b>%s</b>. 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 <b>%s</b> 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 (<b>%s</b>). 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. <a href="%s">Klikněte zde</a> 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 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">seznamu odcizených hesel</a>, 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 <b>%[1]s</b>, děkujeme za registraci na %[2]s! activate_account.text_2=Pro aktivaci vašeho účtu do <b>%s</b> 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 <b>%s</b> 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=<b>@%s</b> vás zmínil/a: @@ -438,11 +492,11 @@ issue.action.push_n=<b>@%[1]s</b> nahrál/a %[3]d commity do %[2]s issue.action.close=<b>@%[1]s</b> uzavřel/a #%[2]d. issue.action.reopen=<b>@%[1]s</b> znovu otevřel/a #%[2]d. issue.action.merge=<b>@%[1]s</b> sloučil/a #%[2]d do %[3]s. -issue.action.approve=<b>@%[1]s</b> schválil/a tento požadavek na natažení. -issue.action.reject=<b>@%[1]s</b> požadoval/a změny v tomto požadavku na natažení. -issue.action.review=<b>@%[1]s</b> okomentoval/a tento požadavek na natažení. -issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu z %[2]s pro tento požadavek na natažení. -issue.action.ready_for_review=<b>@%[1]s</b> označil/a tento požadavek na natažení jako připravený ke kontrole. +issue.action.approve=<b>@%[1]s</b> schválil/a tento pull request. +issue.action.reject=<b>@%[1]s</b> požadoval/a změny v tomto pull requestu. +issue.action.review=<b>@%[1]s</b> okomentoval/a tento pull request. +issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu z %[2]s pro tento pull request. +issue.action.ready_for_review=<b>@%[1]s</b> označil/a tento pull request jako připravený ke kontrole. issue.action.new=<b>@%[1]s</b> 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 <b>%s</b> 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 „<user> přidal/odstranil <label>“. +hidden_comment_types.ref_tooltip=Komentáře, na které se odkazovalo z jiného úkolu/commitu/… +hidden_comment_types.issue_ref_tooltip=Komentáře, kde uživatel změní větev/značku spojenou s problémem comment_type_group_reference=Reference comment_type_group_label=Štítek comment_type_group_milestone=Milník @@ -631,6 +733,7 @@ comment_type_group_project=Projekt comment_type_group_issue_ref=Referenční číslo úkolu saved_successfully=Vaše nastavení bylo úspěšně uloženo. privacy=Soukromí +keep_activity_private=Skrýt aktivitu z profilové stránky keep_activity_private_popup=Učinit aktivitu viditelnou pouze pro vás a administrátory lookup_avatar_by_mail=Vyhledat avatar pomocí e-mailové adresy @@ -640,12 +743,14 @@ choose_new_avatar=Vybrat nový avatar update_avatar=Aktualizovat avatar delete_current_avatar=Smazat aktuální avatar uploaded_avatar_not_a_image=Nahraný soubor není obrázek. +uploaded_avatar_is_too_big=Nahraný soubor (%d KiB) přesahuje maximální velikost (%d KiB). update_avatar_success=Vaše avatar byl aktualizován. update_user_avatar_success=Uživatelův avatar byl aktualizován. change_password=Aktualizovat heslo old_password=Stávající heslo new_password=Nové heslo +retype_new_password=Potvrdit nové heslo password_incorrect=Zadané heslo není správné. change_password_success=Vaše heslo bylo aktualizováno. Od teď se přihlašujte novým heslem. password_change_disabled=Externě ověřovaní uživatelé nemohou aktualizovat své heslo prostřednictvím webového rozhraní Gitea. @@ -654,6 +759,7 @@ emails=E-mailová adresa manage_emails=Správa e-mailových adres manage_themes=Vyberte výchozí motiv vzhledu manage_openid=Správa OpenID adres +email_desc=Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla, a pokud není skrytá, pro operace Gitu. theme_desc=Toto bude váš výchozí motiv vzhledu napříč stránkou. primary=Hlavní activated=Aktivován @@ -661,6 +767,7 @@ requires_activation=Vyžaduje aktivaci primary_email=Nastavit jako hlavní activate_email=Odeslat aktivaci activations_pending=Čekající aktivace +can_not_add_email_activations_pending=Existuje čekající aktivace, zkuste to znovu za pár minut, pokud chcete přidat nový e-mail. delete_email=Smazat email_deletion=Odstranit e-mailovou adresu email_deletion_desc=E-mailová adresa a přidružené informace budou z vašeho účtu odstraněny. Commity Gitu s touto e-mailovou adresou zůstanou nezměněny. Pokračovat? @@ -674,10 +781,12 @@ add_new_email=Přidat novou e-mailovou adresu add_new_openid=Přidat novou OpenID URI add_email=Přidat e-mailovou adresu add_openid=Přidat OpenID URI +add_email_confirmation_sent=Potvrzovací e-mail byl odeslán na „%s“. Prosím zkontrolujte příchozí poštu během následujících %s pro potvrzení vaší e-mailové adresy. add_email_success=Nová e-mailová adresa byla přidána. email_preference_set_success=Nastavení e-mailu bylo úspěšně nastaveno. add_openid_success=Nová OpenID adresa byla přidána. keep_email_private=Schovat e-mailovou adresu +keep_email_private_popup=Toto skryje vaši e-mailovou adresu z vašeho profilu, stejně jako při vytvoření pull requestu nebo úpravě souboru pomocí webového rozhraní. Odeslané commity nebudou změněny. Použijte %s v commitech pro jejich přiřazení k vašemu účtu. openid_desc=OpenID vám umožní delegovat ověřování na externího poskytovatele. manage_ssh_keys=Správa klíčů SSH @@ -708,10 +817,9 @@ gpg_invalid_token_signature=Zadaný GPG klíč, podpis a token se neshodují neb gpg_token_required=Musíte zadat podpis pro níže uvedený token gpg_token=Token gpg_token_help=Podpis můžete vygenerovat pomocí: -gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Zakódovaný podpis GPG key_signature_gpg_placeholder=Začíná s „-----BEGIN PGP SIGNATURE-----“ -verify_gpg_key_success=GPG klíč "%s" byl ověřen. +verify_gpg_key_success=GPG klíč „%s“ byl ověřen. ssh_key_verified=Ověřený klíč ssh_key_verified_long=Klíč byl ověřen pomocí tokenu a může být použit k ověření commitů shodujících se s libovolnou vaší aktivovanou e-mailovou adresou pro tohoto uživatele. ssh_key_verify=Ověřit @@ -721,14 +829,15 @@ ssh_token=Token ssh_token_help=Podpis můžete vygenerovat pomocí: ssh_token_signature=Zakódovaný podpis SSH key_signature_ssh_placeholder=Začíná s „-----BEGIN SSH SIGNATURE-----“ -verify_ssh_key_success=SSH klíč "%s" byl ověřen. +verify_ssh_key_success=SSH klíč „%s“ byl ověřen. subkeys=Podklíče key_id=ID klíče key_name=Název klíče key_content=Obsah principal_content=Obsah -add_key_success=SSH klíč "%s" byl přidán. -add_gpg_key_success=GPG klíč "%s" byl přidán. +add_key_success=SSH klíč „%s“ byl přidán. +add_gpg_key_success=GPG klíč „%s“ byl přidán. +add_principal_success=Byl přidán SSH Principal certifikát „%s“. delete_key=Odstranit ssh_key_deletion=Odstraňte SSH klíč gpg_key_deletion=Odstraňte GPG klíč @@ -755,7 +864,9 @@ ssh_disabled=SSH zakázáno ssh_signonly=SSH je v současné době zakázáno, proto jsou tyto klíče použity pouze pro ověření podpisu. ssh_externally_managed=Tento SSH klíč je spravován externě pro tohoto uživatele manage_social=Správa propojených účtů sociálních sítí +social_desc=Tyto účty sociálních sítí lze použít k přihlášení k vašemu účtu. Ujistěte se, že jsou všechny vaše. unbind=Odpojit +unbind_success=Účet sociální sítě byl úspěšně odstraněn. manage_access_token=Spravovat přístupové tokeny generate_new_token=Vygenerovat nový token @@ -776,6 +887,8 @@ permissions_access_all=Vše (veřejné, soukromé a omezené) select_permissions=Vyberte oprávnění permission_no_access=Bez přístupu permission_read=Přečtené +permission_write=čtení i zápis +access_token_desc=Vybraná oprávnění tokenu omezují autorizaci pouze na odpovídající trasy <a %s>API</a>. Přečtěte si <a %s>dokumentaci</a> pro více informací. at_least_one_permission=Musíte vybrat alespoň jedno oprávnění pro vytvoření tokenu permissions_list=Oprávnění: @@ -787,6 +900,8 @@ remove_oauth2_application_desc=Odstraněním OAuth2 aplikace odeberete přístup remove_oauth2_application_success=Aplikace byla odstraněna. create_oauth2_application=Vytvořit novou OAuth2 aplikaci create_oauth2_application_button=Vytvořit aplikaci +create_oauth2_application_success=Úspěšně jste vytvořili novou OAuth2 aplikaci. +update_oauth2_application_success=Úspěšně jste aktualizovali OAuth2 aplikaci. oauth2_application_name=Název aplikace oauth2_confidential_client=Důvěrný klient. Vyberte aplikace, které zachovávají důvěrnosti v utajení, jako jsou webové aplikace. Nevybírejte pro nativní aplikace včetně stolních a mobilních aplikací. oauth2_redirect_uris=Přesměrování URI. Použijte nový řádek pro každou URI. @@ -795,19 +910,26 @@ oauth2_client_id=ID klienta oauth2_client_secret=Tajný klíč klienta oauth2_regenerate_secret=Obnovit tajný klíč oauth2_regenerate_secret_hint=Ztratili jste svůj tajný klíč? +oauth2_client_secret_hint=Tajný klíč se znovu nezobrazí po opuštění nebo obnovení této stránky. Ujistěte se, že jste si jej uložili. oauth2_application_edit=Upravit oauth2_application_create_description=OAuth2 aplikace poskytuje přístup aplikacím třetích stran k uživatelským účtům na této instanci. +oauth2_application_remove_description=Odebráním OAuth2 aplikace zabrání přístupu ověřeným uživatelům na této instanci. Pokračovat? +oauth2_application_locked=Gitea předregistruje některé OAuth2 aplikace při spuštění, pokud je to povoleno v konfiguraci. Aby se zabránilo neočekávanému chování, nelze je upravovat ani odstranit. Více informací naleznete v dokumentaci OAuth2. authorized_oauth2_applications=Autorizovat OAuth2 aplikaci +authorized_oauth2_applications_description=Úspěšně jste povolili přístup k vašemu osobnímu účtu této aplikaci třetí strany. Zrušte prosím přístup aplikacím, které již nadále nepotřebujete. revoke_key=Zrušit revoke_oauth2_grant=Zrušit přístup revoke_oauth2_grant_description=Zrušením přístupu této aplikaci třetí strany ji zabráníte v přístupu k vašim datům. Jste si jisti? +revoke_oauth2_grant_success=Přístup byl úspěšně zrušen. twofa_desc=Dvoufaktorový způsob ověřování zvýší zabezpečení vašeho účtu. +twofa_recovery_tip=Pokud ztratíte své zařízení, budete moci použít jednorázový obnovovací klíč k získání přístupu k vašemu účtu. twofa_is_enrolled=Váš účet aktuálně <strong>používá</strong> dvoufaktorové ověřování. twofa_not_enrolled=Váš účet aktuálně nepoužívá dvoufaktorové ověřování. twofa_disable=Zakázat dvoufaktorové ověřování twofa_scratch_token_regenerate=Obnovit pomocný token +twofa_scratch_token_regenerated=Váš jednorázový obnovovací klíč je nyní %s. Uložte jej na bezpečném místě, protože se znovu nezobrazí. twofa_enroll=Povolit dvoufaktorové ověřování twofa_disable_note=Dvoufaktorové ověřování můžete zakázat, když bude potřeba. twofa_disable_desc=Zakážete-li dvoufaktorové ověřování, bude váš účet méně zabezpečený. Pokračovat? @@ -825,6 +947,8 @@ webauthn_register_key=Přidat bezpečnostní klíč webauthn_nickname=Přezdívka webauthn_delete_key=Odstranit bezpečnostní klíč webauthn_delete_key_desc=Pokud odstraníte bezpečnostní klíč, již se s ním nebudete moci přihlásit. Pokračovat? +webauthn_key_loss_warning=Pokud ztratíte své bezpečnostní klíče, ztratíte přístup k vašemu účtu. +webauthn_alternative_tip=Možná budete chtít nakonfigurovat další metodu ověřování. manage_account_links=Správa propojených účtů manage_account_links_desc=Tyto externí účty jsou propojeny s vaším Gitea účtem. @@ -834,8 +958,10 @@ remove_account_link=Odstranit propojený účet remove_account_link_desc=Odstraněním propojeného účtu zrušíte jeho přístup k vašemu Gitea účtu. Pokračovat? remove_account_link_success=Propojený účet byl odstraněn. +hooks.desc=Přidat webhooky, které budou spouštěny pro <strong>všechny repozitáře</strong> vve vašem vlastnictví. orgs_none=Nejste členem žádné organizace. +repos_none=Nevlastníte žádné repozitáře. delete_account=Smazat váš účet delete_prompt=Tato operace natrvalo odstraní váš uživatelský účet. <strong>NELZE</strong> ji vrátit zpět. @@ -854,9 +980,12 @@ visibility=Viditelnost uživatele visibility.public=Veřejný visibility.public_tooltip=Viditelné pro všechny visibility.limited=Omezený +visibility.limited_tooltip=Viditelné pouze pro ověřené uživatele visibility.private=Soukromý +visibility.private_tooltip=Viditelné pouze pro členy organizací, ke kterým jste se připojili [repo] +new_repo_helper=Repozitář obsahuje všechny projektové soubory, včetně historie revizí. Už jej hostujete jinde? <a href="%s">Migrovat repozitář.</a> owner=Vlastník owner_helper=Některé organizace se nemusejí v seznamu zobrazit kvůli maximálnímu dosaženému počtu repozitářů. repo_name=Název repozitáře @@ -868,6 +997,7 @@ template_helper=Z repozitáře vytvořit šablonu template_description=Šablony repozitářů umožňují uživatelům generovat nové repositáře se stejnou strukturou, soubory a volitelnými nastaveními. visibility=Viditelnost visibility_description=Pouze majitelé nebo členové organizace to budou moci vidět, pokud mají práva. +visibility_helper=Nastavit repozitář jako soukromý visibility_helper_forced=Váš administrátor vynutil, že nové repozitáře budou soukromé. visibility_fork_helper=(Změna tohoto ovlivní všechny rozštěpení repozitáře.) clone_helper=Potřebujete pomoci s klonováním? Navštivte <a target="_blank" rel="noopener noreferrer" href="%s">nápovědu</a>. @@ -876,8 +1006,12 @@ fork_from=Rozštěpit z already_forked=Již jsi rozštěpil %s fork_to_different_account=Rozštěpit na jiný účet fork_visibility_helper=Viditelnost rozštěpeného repozitáře nemůže být změněna. +fork_branch=Větev, která má být klonována pro fork +all_branches=Všechny větve +fork_no_valid_owners=Tento repozitář nemůže být rozštěpen, protože neexistují žádní platní vlastníci. +fork.blocked_user=Nelze rozštěpit repozitář, protože jste blokováni majitelem repozitáře. use_template=Použít tuto šablonu -clone_in_vsc=Klonovat ve VS Code +open_with_editor=Otevřít pomocí %s download_zip=Stáhnout ZIP download_tar=Stáhnout TAR.GZ download_bundle=Stáhnout BUNDLE @@ -893,6 +1027,8 @@ issue_labels_helper=Vyberte sadu štítků úkolů. license=Licence license_helper=Vyberte licenční soubor. license_helper_desc=Licence řídí, co ostatní mohou a nemohou dělat s vaším kódem. Nejste si jisti, která je pro váš projekt správná? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">Zvolte licenci</a> +object_format=Formát objektu +object_format_helper=Objektový formát repozitáře. Nelze později změnit. SHA1 je nejvíce kompatibilní. readme=README readme_helper=Vyberte šablonu souboru README. readme_helper_desc=Toto je místo, kde můžete napsat úplný popis vašeho projektu. @@ -904,14 +1040,18 @@ trust_model_helper_collaborator_committer=Spolupracovník+Přispěvatel: Důvě trust_model_helper_default=Výchozí: Použít výchozí model důvěry pro tuto instalaci create_repo=Vytvořit repozitář default_branch=Výchozí větev -default_branch_helper=Výchozí větev je základní větev pro požadavky na natažení a commity kódu. +default_branch_label=výchozí +default_branch_helper=Výchozí větev je základní větev pro pull requesty a commity kódu. mirror_prune=Vyčistit mirror_prune_desc=Odstranit zastaralé reference na vzdálené sledování mirror_interval=Interval zrcadlení (platné časové jednotky jsou „h“, „m“ a „s“). 0 zakáže periodickou synchronizaci. (Minimální interval: %s) mirror_interval_invalid=Interval zrcadlení není platný. +mirror_sync=synchronizováno mirror_sync_on_commit=Synchronizovat při nahrávání revizí mirror_address=Klonovat z URL mirror_address_desc=Zadejte požadované přístupové údaje do sekce Ověření. +mirror_address_url_invalid=Poskytnutá URL je neplatná. Všechny části musíte správně nahradit escape sekvencí. +mirror_address_protocol_invalid=Zadaná URL je neplatná. Mohou být zrcadleny pouze umístění http(s):// nebo git://. mirror_lfs=Úložiště velkých souborů (LFS) mirror_lfs_desc=Aktivovat zrcadlení dat LFS. mirror_lfs_endpoint=Koncový bod LFS @@ -937,19 +1077,27 @@ delete_preexisting=Odstranit již existující soubory delete_preexisting_content=Odstranit soubory v %s delete_preexisting_success=Smazány nepřijaté soubory v %s blame_prior=Zobrazit blame před touto změnou +blame.ignore_revs=Ignorování revizí v <a href="%s">.git-blame-ignorerevs</a>. Klikněte zde <a href="%s">pro obejití</a> a zobrazení normálního pohledu blame. +blame.ignore_revs.failed=Nepodařilo se ignorovat revize v <a href="%s">.git-blame-ignore-revs</a>. author_search_tooltip=Zobrazí maximálně 30 uživatelů +tree_path_not_found_commit=Cesta %[1]s v commitu %[2]s neexistuje +tree_path_not_found_branch=Cesta %[1]s ve větvi %[2]s neexistuje +tree_path_not_found_tag=Cesta %[1]s ve značce %[2]s neexistuje transfer.accept=Přijmout převod transfer.accept_desc=Převést do „%s“ transfer.reject=Odmítnout převod transfer.reject_desc=Zrušit převod do „%s“ +transfer.no_permission_to_accept=Nemáte oprávnění k přijetí tohoto převodu. +transfer.no_permission_to_reject=Nemáte oprávnění k odmítnutí tohoto převodu. desc.private=Soukromý desc.public=Veřejný desc.template=Šablona desc.internal=Interní desc.archived=Archivováno +desc.sha256=SHA256 template.items=Položky šablony template.git_content=Obsah gitu (výchozí větev) @@ -962,12 +1110,15 @@ template.issue_labels=Štítky úkolů template.one_item=Musíte vybrat alespoň jednu položku šablony template.invalid=Musíte vybrat repositář šablony +archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo pull requesty. +archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo pull requesty. archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly. -archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat požadavky na natažení. +archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat pull requesty. form.reach_limit_of_creation_1=Již jste dosáhli svůj limit %d repozitář. form.reach_limit_of_creation_n=Již jste dosáhli svůj limit %d repozitářů. form.name_reserved=Název repozitáře „%s“ je rezervován. +form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu repozitáře. need_auth=Ověření migrate_options=Možnosti migrace @@ -977,12 +1128,13 @@ migrate_options_lfs=Migrovat LFS soubory migrate_options_lfs_endpoint.label=Koncový bod LFS migrate_options_lfs_endpoint.description=Migrace se pokusí použít váš vzdálený Git pro <a target="_blank" rel="noopener noreferrer" href="%s">určení LFS serveru</a>. Můžete také zadat vlastní koncový bod, pokud jsou data LFS repozitáře uložena někde jinde. migrate_options_lfs_endpoint.description.local=Podporována je také cesta k lokálnímu serveru. +migrate_options_lfs_endpoint.placeholder=Ponecháte-li prázdné, koncový bod bude odvozen z adresy URL klonu migrate_items=Položky pro migrování migrate_items_wiki=Wiki migrate_items_milestones=Milníky migrate_items_labels=Štítky migrate_items_issues=Úkoly -migrate_items_pullrequests=Požadavky na natažení +migrate_items_pullrequests=Pull requesty migrate_items_merge_requests=Sloučit požadavky migrate_items_releases=Vydání migrate_repo=Migrovat repozitář @@ -992,6 +1144,7 @@ migrate.github_token_desc=Můžete sem vložit jeden nebo více tokenů oddělen migrate.clone_local_path=nebo místní cesta serveru migrate.permission_denied=Není dovoleno importovat místní repozitáře. migrate.permission_denied_blocked=Nelze importovat z nepovolených hostitelů, prosím požádejte správce, aby zkontroloval nastavení ALLOWED_DOMAINS/ALLOW_LOCALETWORKS/BLOCKED_DOMAINS. +migrate.invalid_local_path=Místní cesta je neplatná, buď neexistuje nebo není adresářem. migrate.invalid_lfs_endpoint=Koncový bod LFS není platný. migrate.failed=Přenesení selhalo: %v migrate.migrate_items_options=Pro migraci dalších položek je vyžadován přístupový token @@ -1016,7 +1169,7 @@ migrate.migrating_milestones=Migrování milnků migrate.migrating_labels=Migrování štítků migrate.migrating_releases=Migrování vydání migrate.migrating_issues=Migrování úkolů -migrate.migrating_pulls=Migrování požadavků na natažení +migrate.migrating_pulls=Migrování pull requestů migrate.cancel_migrating_title=Zrušit migraci migrate.cancel_migrating_confirm=Chcete zrušit tuto migraci? @@ -1032,6 +1185,7 @@ watch=Sledovat unstar=Odoblíbit star=Oblíbit fork=Rozštěpit +action.blocked_user=Nelze provést akci, protože jste zablokování vlastníkem repozitáře. download_archive=Stáhnout repozitář more_operations=Další operace @@ -1054,7 +1208,7 @@ find_tag=Najít značku branches=Větve tags=Značky issues=Úkoly -pulls=Požadavky na natažení +pulls=Pull requesty project_board=Projekty packages=Balíčky actions=Akce @@ -1069,6 +1223,7 @@ release=Vydání releases=Vydání tag=Značka released_this=vydal/a toto +tagged_this=označil/a file.title=%s v %s file_raw=Surový file_history=Historie @@ -1077,6 +1232,10 @@ file_view_rendered=Zobrazit vykreslené file_view_raw=Zobrazit v surovém stavu file_permalink=Trvalý odkaz file_too_large=Soubor je příliš velký pro zobrazení. +invisible_runes_header=`Tento soubor obsahuje neviditelné znaky Unicode` +invisible_runes_description=`Tento soubor obsahuje neviditelné Unicode znaky, které jsou pro člověka nerozeznatelné, ale mohou být zpracovány jiným způsobem. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.` +ambiguous_runes_header=`Tento soubor obsahuje nejednoznačné znaky Unicode` +ambiguous_runes_description=`Tento soubor obsahuje znaky Unicode, které mohou být zaměněny s jinými znaky. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.` invisible_runes_line=`Tento řádek má neviditelné znaky Unicode` ambiguous_runes_line=`Tento řádek má nejednoznačné znaky Unicode` ambiguous_character=`%[1]c [U+%04[1]X] je zaměnitelný s %[2]c [U+%04[2]X]` @@ -1089,11 +1248,17 @@ video_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5 audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5 audio. stored_lfs=Uloženo pomocí Git LFS symbolic_link=Symbolický odkaz +executable_file=Spustitelný soubor +vendored=Vendorováno +generated=Generováno commit_graph=Graf commitů commit_graph.select=Vybrat větve -commit_graph.hide_pr_refs=Skrýt požadavky na natažení +commit_graph.hide_pr_refs=Skrýt pull requesty commit_graph.monochrome=Černobílé commit_graph.color=Barva +commit.contained_in=Tento commit je obsažen v: +commit.contained_in_default_branch=Tento commit je součástí výchozí větve +commit.load_referencing_branches_and_tags=Načíst větve a značky odkazující na tento commit blame=Blame download_file=Stáhnout soubor normal_view=Normální zobrazení @@ -1127,11 +1292,12 @@ editor.update=Aktualizovat %s editor.delete=Odstranit %s editor.patch=Použít záplatu editor.patching=Záplatování: +editor.fail_to_apply_patch=Nelze použít záplatu „%s“ editor.new_patch=Nová záplata editor.commit_message_desc=Přidat volitelný rozšířený popis… editor.signoff_desc=Přidat Signed-off-by podpis přispěvatele na konec zprávy o commitu. editor.commit_directly_to_this_branch=Odevzdat přímo do větve <strong class="branch-name">%s</strong>. -editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a spustit požadavek na natažení. +editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a začít pull request. editor.create_new_branch_np=Vytvořte <strong>novou větev</strong> z tohoto commitu. editor.propose_file_change=Navrhnout změnu souboru editor.new_branch_name=Pojmenujte novou větev pro tento commit @@ -1141,7 +1307,14 @@ editor.filename_cannot_be_empty=Jméno nemůže být prázdné. editor.filename_is_invalid=Název souboru je neplatný: „%s“. editor.branch_does_not_exist=Větev „%s“ v tomto repozitáři neexistuje. editor.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři. +editor.directory_is_a_file=Jméno adresáře „%s“ je již použito jako jméno souboru v tomto repozitáři. +editor.file_is_a_symlink=`„%s“ je symbolický odkaz. Symbolické odkazy nemohou být upravovány ve webovém editoru` +editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři. +editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již není součástí tohoto repozitáře. +editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není součástí tohoto repozitáře. editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte zde</a>, abyste je zobrazili, nebo <strong>potvrďte změny ještě jednou</strong> pro jejich přepsání. +editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři. +editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi začal/a s úpravami. Odevzdat do záplatové větve a poté sloučit. editor.commit_empty_file_header=Odevzdat prázdný soubor editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat? editor.no_changes_to_show=Žádné změny k zobrazení. @@ -1163,9 +1336,10 @@ editor.revert=Vrátit %s na: commits.desc=Procházet historii změn zdrojového kódu. commits.commits=Commity +commits.no_commits=Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii. commits.nothing_to_compare=Tyto větve jsou stejné. -commits.search=Hledání commitů… -commits.find=Vyhledat +commits.search.tooltip=Můžete předřadit klíčová slova s „author:“, „committer:“, „after:“ nebo „before:“, např. „revert author:Alice before:2019-01-03“. +commits.search_branch=Tato větev commits.search_all=Všechny větve commits.author=Autor commits.message=Zpráva @@ -1177,6 +1351,7 @@ commits.signed_by_untrusted_user=Podepsáno nedůvěryhodným uživatelem commits.signed_by_untrusted_user_unmatched=Podepsáno nedůvěryhodným uživatelem, který nesouhlasí s přispěvatelem commits.gpg_key_id=ID GPG klíče commits.ssh_key_fingerprint=Otisk klíče SSH +commits.view_path=Zobrazit v tomto bodě v historii commit.operations=Operace commit.revert=Vrátit @@ -1202,18 +1377,19 @@ projects.create=Vytvořit projekt projects.title=Název projects.new=Nový projekt projects.new_subheader=Koordinujte, sledujte a aktualizujte svou práci na jednom místě, aby projekty zůstaly transparentní a v plánu. +projects.create_success=Projekt „%s“ byl vytvořen. projects.deletion=Odstranit projekt projects.deletion_desc=Odstranění projektu jej odstraní ze všech souvisejících úkolů. Pokračovat? projects.deletion_success=Projekt byl odstraněn. projects.edit=Upravit projekty projects.edit_subheader=Projekty organizují úkoly a sledují pokrok. projects.modify=Aktualizovat projekt +projects.edit_success=Projekt „%s“ byl aktualizován. projects.type.none=Žádný projects.type.basic_kanban=Základní Kanban projects.type.bug_triage=Třídění chyb projects.template.desc=Šablona projektu projects.template.desc_helper=Vyberte šablonu projektu pro začátek -projects.type.uncategorized=Nezařazené projects.column.edit=Upravit sloupec projects.column.edit_title=Název projects.column.new_title=Název @@ -1221,10 +1397,7 @@ projects.column.new_submit=Vytvořit sloupec projects.column.new=Nový sloupec projects.column.set_default=Nastavit jako výchozí projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekategorizované úkoly a požadavky na natažení -projects.column.unset_default=Zrušit nastavení jako výchozí -projects.column.unset_default_desc=Zrušit nastavení tohoto sloupce jako výchozí projects.column.delete=Smazat sloupec -projects.column.deletion_desc=Smazání projektového sloupce přesune všechny související problémy do kategorie „Nezařazené“. Pokračovat? projects.column.color=Barva projects.open=Otevřít projects.close=Zavřít @@ -1233,7 +1406,7 @@ projects.card_type.desc=Náhledy karet projects.card_type.images_and_text=Obrázky a text projects.card_type.text_only=Pouze text -issues.desc=Organizování hlášení chyb, úkolů a milníků. +issues.desc=Organizování hlášení chyb, úloh a milníků. issues.filter_assignees=Filtrovat zpracovatele issues.filter_milestones=Filtrovat milník issues.filter_projects=Filtrovat projekt @@ -1259,12 +1432,15 @@ issues.new.assignees=Zpracovatelé issues.new.clear_assignees=Smazat zpracovatele issues.new.no_assignees=Bez zpracovatelů issues.new.no_reviewers=Žádní posuzovatelé +issues.new.blocked_user=Nemůžete vytvořit úkol, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. +issues.edit.blocked_user=Nemůžete upravovat obsah, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. issues.choose.get_started=Začínáme issues.choose.open_external_link=Otevřít issues.choose.blank=Výchozí issues.choose.blank_about=Vytvořit úkol z výchozí šablony. issues.choose.ignore_invalid_templates=Neplatné šablony byly ignorovány issues.choose.invalid_templates=%v nalezených neplatných šablon +issues.choose.invalid_config=Nastavení problému obsahuje chyby: issues.no_ref=Není určena žádná větev/značka issues.create=Vytvořit úkol issues.new_label=Nový štítek @@ -1301,6 +1477,7 @@ issues.delete_branch_at=`odstranil/a větev <b>%s</b> %s` issues.filter_label=Štítek issues.filter_label_exclude=`Chcete-li vyloučit štítky, použijte <code>alt</code> + <code>click/enter</code>` issues.filter_label_no_select=Všechny štítky +issues.filter_label_select_no_label=Bez štítku issues.filter_milestone=Milník issues.filter_milestone_all=Všechny milníky issues.filter_milestone_none=Žádné milníky @@ -1354,6 +1531,7 @@ issues.next=Další issues.open_title=otevřený issues.closed_title=zavřený issues.draft_title=Koncept +issues.num_comments_1=%d komentář issues.num_comments=%d komentářů issues.commented_at=`okomentoval <a href="#%s">%s</a>` issues.delete_comment_confirm=Jste si jist, že chcete smazat tento komentář? @@ -1362,25 +1540,37 @@ issues.context.quote_reply=Citovat odpověď issues.context.reference_issue=Odkázat v novém úkolu issues.context.edit=Upravit issues.context.delete=Smazat +issues.no_content=K dispozici není žádný popis. issues.close=Zavřít problém +issues.comment_pull_merged_at=sloučený commit %[1]s do %[2]s %[3]s issues.comment_manually_pull_merged_at=ručně sloučený commit %[1]s do %[2]s %[3]s issues.close_comment_issue=Okomentovat a zavřít issues.reopen_issue=Znovuotevřít issues.reopen_comment_issue=Okomentovat a znovuotevřít issues.create_comment=Okomentovat +issues.comment.blocked_user=Nemůžete vytvořit nebo upravovat komentář, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. issues.closed_at=`uzavřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`znovuotevřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`odkázal na tento úkol z commitu <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_issue_from=`<a href="%[3]s">odkazoval/a na tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` -issues.ref_pull_from=`<a href="%[3]s">odkazoval/a na tento požadavek na natažení %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` -issues.ref_closing_from=`<a href="%[3]s">odkazoval/a na požadavek na natažení %[4]s, který uzavře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` -issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na požadavek na natažení %[4]s, který znovu otevře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.ref_pull_from=`<a href="%[3]s">odkazoval/a na tento pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.ref_closing_from=`<a href="%[3]s">odkazoval/a na pull request %[4]s, který uzavře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na pull request %[4]s, který znovu otevře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_reopened_from=`<a href="%[3]s">znovu otevřel/a tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_from=`z %[1]s` issues.author=Autor +issues.author_helper=Tento uživatel je autor. issues.role.owner=Vlastník +issues.role.owner_helper=Tento uživatel je vlastníkem tohoto repozitáře. issues.role.member=Člen +issues.role.member_helper=Tento uživatel je členem organizace vlastnící tento repositář. +issues.role.collaborator=Spolupracovník +issues.role.collaborator_helper=Tento uživatel byl pozván ke spolupráci v repozitáři. +issues.role.first_time_contributor=Přispěvatel, který přispívá poprvé +issues.role.first_time_contributor_helper=Toto je první příspěvek tohoto uživatele do repozitáře. +issues.role.contributor=Přispěvatel +issues.role.contributor_helper=Tento uživatel již dříve přispíval do repozitáře. issues.re_request_review=Znovu požádat o posouzení issues.is_stale=Od tohoto posouzení došlo ke změnám v tomto požadavku na natažení issues.remove_request_review=Odstranit žádost o posouzení @@ -1395,6 +1585,11 @@ issues.label_title=Název štítku issues.label_description=Popis štítku issues.label_color=Barva štítku issues.label_exclusive=Exkluzivní +issues.label_archive=Archivovat štítek +issues.label_archived_filter=Zobrazit archivované popisky +issues.label_archive_tooltip=Archivované štítky jsou ve výchozím nastavení vyloučeny z návrhů při hledání podle popisku. +issues.label_exclusive_desc=Pojmenujte štítek <code>rozsah/položka</code>, aby se stal vzájemně exkluzivním s jinými štítky <code>rozsah/</code>. +issues.label_exclusive_warning=Jakékoliv protichůdné rozsahy štítků budou odstraněny při úpravě štítků u úkolů nebo u pull requestů. issues.label_count=%d štítků issues.label_open_issues=%d otevřených úkolů issues.label_edit=Upravit @@ -1447,6 +1642,7 @@ issues.tracking_already_started=`Již jste spustili sledování času na <a href issues.stop_tracking=Zastavit časovač issues.stop_tracking_history=`ukončil/a práci %s` issues.cancel_tracking=Zahodit +issues.cancel_tracking_history=`zrušil/a sledování času %s` issues.add_time=Přidat čas ručně issues.del_time=Odstranit tento časový záznam issues.add_time_short=Přidat čas @@ -1470,6 +1666,7 @@ issues.due_date_form=rrrr-mm-dd issues.due_date_form_add=Přidat termín dokončení issues.due_date_form_edit=Upravit issues.due_date_form_remove=Odstranit +issues.due_date_not_writer=Potřebujete přístup k zápisu do tohoto repozitáře, abyste mohli aktualizovat datum dokončení problému. issues.due_date_not_set=Žádný termín dokončení. issues.due_date_added=přidal/a termín dokončení %s %s issues.due_date_modified=upravil/a termín termínu z %[2]s na %[1]s %[3]s @@ -1488,26 +1685,27 @@ issues.dependency.remove=Odstranit issues.dependency.remove_info=Odstranit tuto závislost issues.dependency.added_dependency=`přidal/a novou závislost %s` issues.dependency.removed_dependency=`odstranil/a závislost %s` -issues.dependency.pr_closing_blockedby=Uzavření tohoto požadavku na natažení je blokováno následujícími úkoly +issues.dependency.pr_closing_blockedby=Uzavření tohoto pull requestu je blokováno následujícími úkoly issues.dependency.issue_closing_blockedby=Uzavření tohoto úkolu je blokováno následujícími úkoly issues.dependency.issue_close_blocks=Tento úkol blokuje uzavření následujících úkolů -issues.dependency.pr_close_blocks=Tento požadavek na natažení blokuje uzavření následujících úkolů +issues.dependency.pr_close_blocks=Tento pull request blokuje uzavření následujících úkolů issues.dependency.issue_close_blocked=Musíte zavřít všechny úkoly, které blokují tento úkol, aby jej bylo možné zavřít. -issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento požadavek na natažení, aby jej bylo možné sloučit. +issues.dependency.issue_batch_close_blocked=Nelze uzavřít úkoly, které jste vybrali, protože úkol #%d má stále otevřené závislosti +issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento pull request, aby jej bylo možné sloučit. issues.dependency.blocks_short=Blokuje issues.dependency.blocked_by_short=Závisí na issues.dependency.remove_header=Odstranit závislost issues.dependency.issue_remove_text=Tímto krokem odeberete závislost z úkolu. Pokračovat? -issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z požadavku na natažení. Pokračovat? -issues.dependency.setting=Povolit závislosti pro úkoly a požadavky na natažení +issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z pull requestu. Pokračovat? +issues.dependency.setting=Povolit závislosti pro úkoly a pull requesty issues.dependency.add_error_same_issue=Úkol nemůže záviset sám na sobě. issues.dependency.add_error_dep_issue_not_exist=Související úkol neexistuje. issues.dependency.add_error_dep_not_exist=Závislost neexistuje. issues.dependency.add_error_dep_exists=Závislost již existuje. issues.dependency.add_error_cannot_create_circular=Nemůžete vytvořit závislost dvou úkolů, které se vzájemně blokují. issues.dependency.add_error_dep_not_same_repo=Oba úkoly musí být ve stejném repozitáři. -issues.review.self.approval=Nemůžete schválit svůj požadavek na natažení. -issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním požadavku na natažení. +issues.review.self.approval=Nemůžete schválit svůj pull request. +issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním pull requestu. issues.review.approve=schválil tyto změny %s issues.review.comment=posoudil %s issues.review.dismissed=zamítl/a posouzení od %s %s @@ -1524,6 +1722,9 @@ issues.review.pending.tooltip=Tento komentář není momentálně viditelný pro issues.review.review=Posouzení issues.review.reviewers=Posuzovatelé issues.review.outdated=Zastaralé +issues.review.outdated_description=Obsah se změnil od chvíle, kdy byl tento komentář vytvořen +issues.review.option.show_outdated_comments=Zobrazit zastaralé komentáře +issues.review.option.hide_outdated_comments=Skrýt zastaralé komentáře issues.review.show_outdated=Zobrazit zastaralé issues.review.hide_outdated=Skrýt zastaralé issues.review.show_resolved=Zobrazit vyřešené @@ -1544,10 +1745,11 @@ issues.reference_link=Reference: %s compare.compare_base=základní compare.compare_head=porovnat -pulls.desc=Povolit požadavky na natažení a posuzování kódu. -pulls.new=Nový požadavek na natažení -pulls.view=Zobrazit požadavek na natažení -pulls.compare_changes=Nový požadavek na natažení +pulls.desc=Povolit pull requesty a posuzování kódu. +pulls.new=Nový pull request +pulls.new.blocked_user=Nemůžete vytvořit pull request, protože jste zablokování vlastníkem repozitáře. +pulls.view=Zobrazit pull request +pulls.compare_changes=Nový pull request pulls.allow_edits_from_maintainers=Povolit úpravy od správců pulls.allow_edits_from_maintainers_desc=Uživatelé s přístupem k zápisu do základní větve mohou také nahrávat do této větve pulls.allow_edits_from_maintainers_err=Aktualizace se nezdařila @@ -1562,38 +1764,53 @@ pulls.compare_compare=natáhnout z pulls.switch_comparison_type=Přepnout typ porovnání pulls.switch_head_and_base=Prohodit hlavní a základní větev pulls.filter_branch=Filtrovat větev -pulls.no_results=Nebyly nalezeny žádné výsledky. -pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení. -pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný. -pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>` -pulls.create=Vytvořit požadavek na natažení +pulls.show_all_commits=Zobrazit všechny commity +pulls.show_changes_since_your_last_review=Zobrazit změny od vašeho posledního posouzení +pulls.showing_only_single_commit=Zobrazuji pouze změny commitu %[1]s +pulls.showing_specified_commit_range=Zobrazují se pouze změny mezi %[1]s..%[2]s +pulls.select_commit_hold_shift_for_range=Vyberte commit. Podržte klávesu shift + klepněte pro výběr rozsahu +pulls.review_only_possible_for_full_diff=Posouzení je možné pouze při zobrazení plného rozlišení +pulls.filter_changes_by_commit=Filtrovat podle commitu +pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet pull request. +pulls.nothing_to_compare_have_tag=Vybraná větev/značka je stejná. +pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento pull request bude prázdný. +pulls.has_pull_request=`Pull request mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>` +pulls.create=Vytvořit pull request pulls.title_desc=chce sloučit %[1]d commity z větve <code>%[2]s</code> do <code id="branch_target">%[3]s</code> pulls.merged_title_desc=sloučil %[1]d commity z větve <code>%[2]s</code> do větve <code>%[3]s</code> před %[4]s pulls.change_target_branch_at=`změnil/a cílovou větev z <b>%s</b> na <b>%s</b> %s` pulls.tab_conversation=Konverzace pulls.tab_commits=Commity pulls.tab_files=Změněné soubory -pulls.reopen_to_merge=Prosíme, otevřete znovu tento požadavek na natažení, aby se provedlo sloučení. -pulls.cant_reopen_deleted_branch=Tento požadavek na natažení nemůže být znovu otevřen protože větev byla smazána. +pulls.reopen_to_merge=Prosíme, otevřete znovu tento pull request, aby se provedlo sloučení. +pulls.cant_reopen_deleted_branch=Tento pull request nemůže být znovu otevřen protože větev byla smazána. pulls.merged=Sloučený +pulls.merged_success=Pull request byl úspěšně sloučen a uzavřen +pulls.closed=Pull request uzavřen pulls.manually_merged=Sloučeno ručně pulls.merged_info_text=Větev %s může být nyní odstraněna. -pulls.is_closed=Požadavek na natažení byl uzavřen. -pulls.title_wip_desc=`<a href="#">Začněte název s <strong>%s</strong></a> a zamezíte tak nechtěnému sloučení požadavku na natažení.` -pulls.cannot_merge_work_in_progress=Tento požadavek na natažení je označen jako probíhající práce. +pulls.is_closed=Pull request byl uzavřen. +pulls.title_wip_desc=`<a href="#">Začněte název s <strong>%s</strong></a> a zamezíte tak nechtěnému sloučení pull requestu.` +pulls.cannot_merge_work_in_progress=Tento pull request je označen jako probíhající práce. pulls.still_in_progress=Stále probíhá? pulls.add_prefix=Přidat prefix <strong>%s</strong> pulls.remove_prefix=Odstranit prefix <strong>%s</strong> -pulls.data_broken=Tento požadavek na natažení je rozbitý kvůli chybějícím informacím o rozštěpení. -pulls.files_conflicted=Tento požadavek na natažení obsahuje změny, které kolidují s cílovou větví. +pulls.data_broken=Tento pull request je rozbitý kvůli chybějícím informacím o rozštěpení. +pulls.files_conflicted=Tento pull request obsahuje změny, které kolidují s cílovou větví. pulls.is_checking=Právě probíhá kontrola konfliktů při sloučení. Zkuste to za chvíli. pulls.is_ancestor=Tato větev je již součástí cílové větve. Není co sloučit. pulls.is_empty=Změny na této větvi jsou již na cílové větvi. Toto bude prázdný commit. pulls.required_status_check_failed=Některé požadované kontroly nebyly úspěšné. pulls.required_status_check_missing=Některé požadované kontroly chybí. -pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento požadavek na natažení. -pulls.can_auto_merge_desc=Tento požadavek na natažení může být automaticky sloučen. -pulls.cannot_auto_merge_desc=Tento požadavek na natažení nemůže být automaticky sloučen, neboť se v něm nachází konflikty. +pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento pull request. +pulls.blocked_by_approvals=Tento pull request ještě nemá dostatek schválení. Uděleno %d z %d schválení. +pulls.blocked_by_rejection=Tento pull request obsahuje změny požadované oficiálním posuzovatelem. +pulls.blocked_by_official_review_requests=Tento pull request obsahuje oficiální žádosti o posouzení. +pulls.blocked_by_outdated_branch=Tento pull request je zablokován, protože je zastaralý. +pulls.blocked_by_changed_protected_files_1=Tento pull request je zablokován, protože mění chráněný soubor: +pulls.blocked_by_changed_protected_files_n=Tento pull request je zablokován, protože mění chráněné soubory: +pulls.can_auto_merge_desc=Tento pull request může být automaticky sloučen. +pulls.cannot_auto_merge_desc=Tento pull request nemůže být automaticky sloučen, neboť se v něm nachází konflikty. pulls.cannot_auto_merge_helper=Pro vyřešení konfliktů proveďte ruční sloučení. pulls.num_conflicting_files_1=%d konfliktní soubor pulls.num_conflicting_files_n=%d konfliktních souborů @@ -1605,20 +1822,21 @@ pulls.waiting_count_1=%d čekající posouzení pulls.waiting_count_n=%d čekající posouzení pulls.wrong_commit_id=ID commitu musí být ID commitu v cílové větvi -pulls.no_merge_desc=Tento požadavek na natažení nemůže být sloučen, protože všechny možnosti repozitáře na sloučení jsou zakázány. -pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení požadavku na natažení ručně. -pulls.no_merge_wip=Požadavek na natažení nemůže být sloučen protože je označen jako nedokončený. -pulls.no_merge_not_ready=Tento požadavek na natažení není připraven na sloučení, zkontrolujte stav posouzení a kontrolu stavu. -pulls.no_merge_access=Nemáte oprávnění sloučit tento požadavek na natažení. +pulls.no_merge_desc=Tento pull request nemůže být sloučen, protože všechny možnosti repozitáře na sloučení jsou zakázány. +pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení pull requestu ručně. +pulls.no_merge_wip=Pull request nemůže být sloučen protože je označen jako nedokončený. +pulls.no_merge_not_ready=Tento pull request není připraven na sloučení, zkontrolujte stav posouzení a kontrolu stavu. +pulls.no_merge_access=Nemáte oprávnění sloučit tento pull request. pulls.merge_pull_request=Vytvořit slučovací commit pulls.rebase_merge_pull_request=Rebase pak fast-forward pulls.rebase_merge_commit_pull_request=Rebase a poté vytvořit slučovací commit pulls.squash_merge_pull_request=Vytvořit squash commit +pulls.fast_forward_only_merge_pull_request=Pouze fast-forward pulls.merge_manually=Sloučeno ručně pulls.merge_commit_id=ID slučovacího commitu pulls.require_signed_wont_sign=Větev vyžaduje podepsané commity, ale toto sloučení nebude podepsáno -pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento požadavek na natažení. +pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento pull request. pulls.merge_conflict=Sloučení selhalo: Došlo ke konfliktu při sloučení. Tip: Zkuste jinou strategii pulls.merge_conflict_summary=Chybové hlášení pulls.rebase_conflict=Sloučení selhalo: Došlo ke konfliktu při rebase commitu: %[1]s. Tip: Zkuste jinou strategii @@ -1626,10 +1844,11 @@ pulls.rebase_conflict_summary=Chybové hlášení pulls.unrelated_histories=Sloučení selhalo: Hlavní a základní revize nesdílí společnou historii. Tip: Zkuste jinou strategii pulls.merge_out_of_date=Sloučení selhalo: Základ byl aktualizován při generování sloučení. Tip: Zkuste to znovu. pulls.head_out_of_date=Sloučení selhalo: Hlavní revize byla aktualizován při generování sloučení. Tip: Zkuste to znovu. +pulls.has_merged=Chyba: Pull request byl sloučen, nelze znovu sloučit nebo změnit cílovou větev. pulls.push_rejected=Sloučení selhalo: Nahrání bylo zamítnuto. Zkontrolujte háčky Gitu pro tento repozitář. pulls.push_rejected_summary=Úplná zpráva o odmítnutí pulls.push_rejected_no_message=Sloučení se nezdařilo: Nahrání bylo odmítnuto, ale nebyla nalezena žádná vzdálená zpráva.<br>Zkontrolujte háčky gitu pro tento repozitář -pulls.open_unmerged_pull_exists=`Nemůžete provést operaci znovuotevření protože je tu čekající požadavek na natažení (#%d) s identickými vlastnostmi.` +pulls.open_unmerged_pull_exists=`Nemůžete provést operaci znovuotevření protože je tu čekající pull request (#%d) s identickými vlastnostmi.` pulls.status_checking=Některé kontroly jsou nedořešeny pulls.status_checks_success=Všechny kontroly byly úspěšné pulls.status_checks_warning=Některé kontroly nahlásily varování @@ -1637,36 +1856,49 @@ pulls.status_checks_failure=Některé kontroly se nezdařily pulls.status_checks_error=Některé kontroly nahlásily chyby pulls.status_checks_requested=Požadováno pulls.status_checks_details=Podrobnosti +pulls.status_checks_hide_all=Skrýt všechny kontroly +pulls.status_checks_show_all=Zobrazit všechny kontroly pulls.update_branch=Aktualizovat větev sloučením pulls.update_branch_rebase=Aktualizovat větev pomocí rebase pulls.update_branch_success=Aktualizace větve byla úspěšná pulls.update_not_allowed=Nemáte oprávnění aktualizovat větev pulls.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi -pulls.closed_at=`uzavřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>` -pulls.reopened_at=`znovuotevřel/a tento požadavek na natažení <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.close=Zavřít pull request +pulls.closed_at=`uzavřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.reopened_at=`znovuotevřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.cmd_instruction_hint=`Zobrazit <a class="show-instruction">instrukce příkazové řádky</a>.` +pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny. +pulls.cmd_instruction_merge_title=Sloučit +pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea. +pulls.clear_merge_message=Vymazat zprávu o sloučení +pulls.clear_merge_message_hint=Vymazání zprávy o sloučení odstraní pouze obsah zprávy a ponechá generované přídavky gitu jako "Co-AuthoreBy …". pulls.auto_merge_button_when_succeed=(Když kontroly uspějí) pulls.auto_merge_when_succeed=Automaticky sloučit, když všechny kontroly uspějí -pulls.auto_merge_newly_scheduled=Požadavek na natažení byl naplánován na sloučení, jakmile všechny kontroly uspějí. -pulls.auto_merge_has_pending_schedule=%[1]s naplánoval/a tento požadavek na natažení pro automatické sloučení, když všechny kontroly uspějí v %[2]s. +pulls.auto_merge_newly_scheduled=Pull request byl naplánován na sloučení, jakmile všechny kontroly uspějí. +pulls.auto_merge_has_pending_schedule=%[1]s naplánoval/a tento pull request pro automatické sloučení, když všechny kontroly uspějí v %[2]s. pulls.auto_merge_cancel_schedule=Zrušit automatické sloučení -pulls.auto_merge_not_scheduled=Tento požadavek na natažení není naplánován na automatické sloučení. -pulls.auto_merge_canceled_schedule=Automatické sloučení bylo zrušeno pro tento požadavek na natažení. +pulls.auto_merge_not_scheduled=Tento pull request není naplánován na automatické sloučení. +pulls.auto_merge_canceled_schedule=Automatické sloučení bylo zrušeno pro tento pull request. -pulls.auto_merge_newly_scheduled_comment=`požadavek na automatické sloučení tohoto požadavku na natažení je naplánován, když všechny kontroly uspějí %[1]s` -pulls.auto_merge_canceled_schedule_comment=`zrušil/a automatické sloučení tohoto požadavku na natažení, když všechny kontroly uspějí %[1]s` +pulls.auto_merge_newly_scheduled_comment=`požadavek na automatické sloučení tohoto pull requestu je naplánován, když všechny kontroly uspějí %[1]s` +pulls.auto_merge_canceled_schedule_comment=`zrušil/a automatické sloučení tohoto pull requestu, když všechny kontroly uspějí %[1]s` -pulls.delete.title=Odstranit tento požadavek na natažení? -pulls.delete.text=Opravdu chcete tento požadavek na natažení smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.) +pulls.delete.title=Odstranit tento pull request? +pulls.delete.text=Opravdu chcete tento pull request smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.) +pulls.recently_pushed_new_branches=Nahráli jste větev <strong>%[1]s</strong> %[2]s +pull.deleted_branch=(odstraněno):%s milestones.new=Nový milník milestones.closed=Zavřen dne %s +milestones.update_ago=Aktualizováno %s milestones.no_due_date=Bez lhůty dokončení milestones.open=Otevřít milestones.close=Zavřít +milestones.new_subheader=Milníky vám pomohou organizovat úkoly a sledovat jejich pokrok. milestones.completeness=%d%% Dokončeno milestones.create=Vytvořit milník milestones.title=Název @@ -1674,18 +1906,35 @@ milestones.desc=Popis milestones.due_date=Termín (volitelný) milestones.clear=Zrušit milestones.invalid_due_date_format=Termín dokončení musí být ve formátu 'rrrr-mm-dd'. +milestones.create_success=Milník „%s“ byl vytvořen. milestones.edit=Upravit milník milestones.edit_subheader=Milník organizuje úkoly a sledují pokrok. milestones.cancel=Zrušit milestones.modify=Aktualizovat milník +milestones.edit_success=Milník „%s“ byl aktualizován. milestones.deletion=Smazat milník milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat? milestones.deletion_success=Milník byl odstraněn. +milestones.filter_sort.earliest_due_data=Nejstarší datum dokončení +milestones.filter_sort.latest_due_date=Nejnovější datum dokončení milestones.filter_sort.least_complete=Nejméně dokončené milestones.filter_sort.most_complete=Nejvíce dokončené milestones.filter_sort.most_issues=Nejvíce úkolů milestones.filter_sort.least_issues=Nejméně úkolů +signing.will_sign=Tento commit bude podepsána klíčem „%s“. +signing.wont_sign.error=Došlo k chybě při kontrole, zda může být commit podepsán. +signing.wont_sign.nokey=K podpisu tohoto commitu není k dispozici žádný klíč. +signing.wont_sign.never=Commity nejsou nikdy podepsány. +signing.wont_sign.always=Commity jsou vždy podepsány. +signing.wont_sign.pubkey=Commit nebude podepsán, protože nemáte veřejný klíč spojený s vaším účtem. +signing.wont_sign.twofa=Pro podepsání commitů musíte mít povoleno dvoufaktorové ověření. +signing.wont_sign.parentsigned=Commit nebude podepsán, protože nadřazený commit není podepsán. +signing.wont_sign.basesigned=Sloučení nebude podepsáno, protože základní commit není podepsaný. +signing.wont_sign.headsigned=Sloučení nebude podepsáno, protože hlavní revize není podepsána. +signing.wont_sign.commitssigned=Sloučení nebude podepsáno, protože všechny přidružené revize nejsou podepsány. +signing.wont_sign.approved=Sloučení nebude podepsáno, protože pull request není schválen. +signing.wont_sign.not_signed_in=Nejste přihlášeni. ext_wiki=Přístup k externí Wiki ext_wiki.desc=Odkaz do externí Wiki. @@ -1709,12 +1958,18 @@ wiki.file_revision=Revize stránky wiki.wiki_page_revisions=Revize Wiki stránky wiki.back_to_wiki=Zpět na wiki stránku wiki.delete_page_button=Smazat stránku +wiki.delete_page_notice_1=Odstranění Wiki stránky „%s“ nemůže být vráceno zpět. Pokračovat? wiki.page_already_exists=Stránka Wiki se stejným názvem již existuje. +wiki.reserved_page=Jméno Wiki stránky „%s“ je rezervováno. wiki.pages=Stránky wiki.last_updated=Naposledy aktualizováno: %s wiki.page_name_desc=Zadejte název této Wiki stránky. Některé speciální názvy jsou: „Home“, „_Sidebar“ a „_Footer“. +wiki.original_git_entry_tooltip=Zobrazit originální Git soubor namísto použití přátelského odkazu. activity=Aktivita +activity.navbar.code_frequency=Frekvence kódu +activity.navbar.contributors=Přispěvatelé +activity.navbar.recent_commits=Nedávné commity activity.period.filter_label=Období: activity.period.daily=1 den activity.period.halfweekly=3 dny @@ -1724,16 +1979,16 @@ activity.period.quarterly=3 měsíce activity.period.semiyearly=6 měsíců activity.period.yearly=1 rok activity.overview=Přehled -activity.active_prs_count_1=<strong>%d</strong> aktivní požadavek na natažení -activity.active_prs_count_n=<strong>%d</strong> aktivní požadavky na natažení -activity.merged_prs_count_1=Sloučený požadavek na natažení -activity.merged_prs_count_n=Sloučené požadavky na natažení -activity.opened_prs_count_1=Navrhovaný požadavek na natažení -activity.opened_prs_count_n=Navrhované požadavky na natažení +activity.active_prs_count_1=<strong>%d</strong> aktivní pull request +activity.active_prs_count_n=<strong>%d</strong> aktivních pull requestů +activity.merged_prs_count_1=Sloučený pull request +activity.merged_prs_count_n=Sloučené pull requesty +activity.opened_prs_count_1=Navrhovaný pull request +activity.opened_prs_count_n=Navrhované pull requesty activity.title.user_1=%d uživatel activity.title.user_n=%d uživatelů -activity.title.prs_1=%d Požadavek na natažení -activity.title.prs_n=%d Požadavků na natažení +activity.title.prs_1=%d pull request +activity.title.prs_n=%d pull requestů activity.title.prs_merged_by=%s sloučil %s activity.title.prs_opened_by=%s navrhl %s activity.merged_prs_label=Sloučený @@ -1752,7 +2007,7 @@ activity.new_issues_count_n=Nové úkoly activity.new_issue_label=Otevřený activity.title.unresolved_conv_1=%d nevyřešená konverzace activity.title.unresolved_conv_n=%d nevyřešených konverzací -activity.unresolved_conv_desc=Tyto nedávno změněné úkolu a požadavky na natažení ještě nebyly vyřešeny. +activity.unresolved_conv_desc=Tyto nedávno změněné úkolu a pull requestu ještě nebyly vyřešeny. activity.unresolved_conv_label=Otevřít activity.title.releases_1=%d Vydání activity.title.releases_n=%d Vydání @@ -1780,16 +2035,10 @@ activity.git_stats_and_deletions=a activity.git_stats_deletion_1=%d odebrání activity.git_stats_deletion_n=%d odebrání -search=Vyhledat -search.search_repo=Hledat repozitář -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 -search.results=Výsledky hledání „%s“ v <a href="%s">%s</a> -search.code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu. -search.code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu. +contributors.contribution_type.filter_label=Typ příspěvku: +contributors.contribution_type.commits=Commity +contributors.contribution_type.additions=Přidání +contributors.contribution_type.deletions=Odstranění settings=Nastavení settings.desc=Nastavení je místo, kde můžete měnit nastavení repozitáře @@ -1804,7 +2053,16 @@ settings.hooks=Webové háčky settings.githooks=Háčky Gitu settings.basic_settings=Základní nastavení settings.mirror_settings=Nastavení zrcadla +settings.mirror_settings.docs=Nastavte repozitář pro automatickou synchronizaci commitů, značek a větví s jiným repozitářem. +settings.mirror_settings.docs.disabled_pull_mirror.instructions=Nastavte váš projekt pro automatické nahrávání commitů, značek a větví do jiného repozitáře. Správce webu zakázal zrcadla pro natažení. +settings.mirror_settings.docs.disabled_push_mirror.instructions=Nastavte svůj projekt pro automatické natažení commitů, značek a větví z jiného repozitáře. +settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=Právě teď to lze provést pouze v menu "Nová migrace". Pro více informací prosím konzultujte: +settings.mirror_settings.docs.disabled_push_mirror.info=Push zrcadla byla zakázána administrátorem vašeho webu. +settings.mirror_settings.docs.no_new_mirrors=Váš repozitář zrcadlí změny do nebo z jiného repozitáře. Mějte prosím na paměti, že v tuto chvíli nemůžete vytvořit žádná nová zrcadla. +settings.mirror_settings.docs.can_still_use=I když nemůžete upravit stávající zrcadla nebo vytvořit nová, stále můžete použít své stávající zrcadlo. +settings.mirror_settings.docs.more_information_if_disabled=Více informací o zrcadlech pro nahrání a natažení naleznete zde: settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře? +settings.mirror_settings.docs.doc_link_pull_section=sekci "stahovat ze vzdáleného úložiště" v dokumentaci. settings.mirror_settings.docs.pulling_remote_title=Stažení ze vzdáleného úložiště settings.mirror_settings.mirrored_repository=Zrcadlený repozitář settings.mirror_settings.direction=Směr @@ -1814,8 +2072,11 @@ settings.mirror_settings.last_update=Poslední aktualizace settings.mirror_settings.push_mirror.none=Nenastavena žádná zrcadla pro nahrání settings.mirror_settings.push_mirror.remote_url=URL vzdáleného Git repozitáře settings.mirror_settings.push_mirror.add=Přidat zrcadlo pro nahrání +settings.mirror_settings.push_mirror.edit_sync_time=Upravit interval synchronizace zrcadla settings.sync_mirror=Synchronizovat nyní +settings.pull_mirror_sync_in_progress=V tuto chvíli probíhá nahrávání změn ze vzdáleného %s. +settings.push_mirror_sync_in_progress=Probíhá nahrávání změn do vzdáleného %s. settings.site=Webová stránka settings.update_settings=Aktualizovat nastavení settings.update_mirror_settings=Aktualizovat nastavení zrcadla @@ -1825,6 +2086,8 @@ settings.branches.add_new_rule=Přidat nové pravidlo settings.advanced_settings=Pokročilá nastavení settings.wiki_desc=Povolit Wiki repozitáře settings.use_internal_wiki=Používat vestavěnou Wiki +settings.default_wiki_branch_name=Výchozí název větve Wiki +settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila. settings.use_external_wiki=Používat externí Wiki settings.external_wiki_url=URL externí Wiki settings.external_wiki_url_error=URL externí wiki platné URL. @@ -1846,13 +2109,20 @@ settings.tracker_issue_style.regexp_pattern_desc=První zachycená skupina bude settings.tracker_url_format_desc=Použijte zástupné symboly <code>{user}</code>, <code>{repo}</code> a <code>{index}</code> pro uživatelské jméno, jméno repozitáře a číslo úkolu. settings.enable_timetracker=Povolit sledování času settings.allow_only_contributors_to_track_time=Povolit sledování času pouze přispěvatelům -settings.pulls_desc=Povolit požadavky na natažení +settings.pulls_desc=Povolit pull requesty repozitáře settings.pulls.ignore_whitespace=Ignorovat bílé znaky při konfliktech settings.pulls.enable_autodetect_manual_merge=Povolit autodetekci ručních sloučení (Poznámka: V některých zvláštních případech může dojít k nesprávnému rozhodnutí) -settings.pulls.allow_rebase_update=Povolit aktualizaci větve požadavku na natažení pomocí rebase -settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat větev požadavku na natažení po jeho sloučení +settings.pulls.allow_rebase_update=Povolit aktualizaci větve pull requestu pomocí rebase +settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat větev pull requestu po jeho sloučení +settings.pulls.default_allow_edits_from_maintainers=Ve výchozím nastavení povolit úpravy od správců +settings.releases_desc=Povolit vydání v repozitáři settings.packages_desc=Povolit registr balíčků repozitáře settings.projects_desc=Povolit projekty v repozitáři +settings.projects_mode_desc=Režim projektů (druhy projektů k zobrazení) +settings.projects_mode_repo=Pouze projekty repozitáře +settings.projects_mode_owner=Pouze projekty uživatele nebo organizace +settings.projects_mode_all=Všechny projekty +settings.actions_desc=Povolit akce repozitáře settings.admin_settings=Nastavení správce settings.admin_enable_health_check=Povolit kontrolu stavu repozitáře (git fsck) settings.admin_code_indexer=Indexování kódu @@ -1877,8 +2147,10 @@ settings.convert_fork_succeed=Rozštěpení bylo překonvertován na běžný re settings.transfer=Předat vlastnictví settings.transfer.rejected=Převod repozitáře byl zamítnut. settings.transfer.success=Převod repozitáře byl úspěšný. +settings.transfer.blocked_user=Nelze převést repozitář, protože jste blokování novým vlastníkem. settings.transfer_abort=Zrušit převod settings.transfer_abort_invalid=Nemůžete zrušit neexistující převod repozitáře. +settings.transfer_abort_success=Převod repozitáře do %s byl úspěšně zrušen. settings.transfer_desc=Předat tento repozitář uživateli nebo organizaci, ve které máte administrátorská práva. settings.transfer_form_title=Zadejte jméno repozitáře pro potvrzení: settings.transfer_in_progress=V současné době probíhá převod. Zrušte jej, pokud chcete převést tento repozitář jinému uživateli. @@ -1898,6 +2170,7 @@ settings.trust_model.collaborator.long=Spolupracovník: Důvěřovat podpisům s settings.trust_model.collaborator.desc=Platné podpisy spolupracovníků tohoto repozitáře budou označeny jako „důvěryhodné“ - (ať se shodují s autorem, či nikoli). V opačném případě budou platné podpisy označeny jako „nedůvěryhodné“, pokud se podpis shoduje s přispěvatelem a „neodpovídající“, pokud ne. settings.trust_model.committer=Přispěvatel settings.trust_model.committer.long=Přispěvatel: Důvěřovat podpisům, které odpovídají autorům (což odpovídá GitHub a přinutí Giteu nastavit jako tvůrce pro Giteou podepsané revize) +settings.trust_model.committer.desc=Platné podpisy budou označeny pouze jako „důvěryhodné“, pokud se shodují s přispěvatelem, jinak budou označeny jako „neodpovídající“. To přinutí Giteu, aby byla přispěvatelem podepsaných commitů se skutečným přispěvatelem označeným jako Co-authored-by: a Co-committed-by: na konci commitu. Výchozí klíč Gitea musí odpovídat uživateli v databázi. settings.trust_model.collaboratorcommitter=Spolupracovník+Přispěvatel settings.trust_model.collaboratorcommitter.long=Spolupracovník+Přispěvatel: Důvěřovat podpisům od spolupracovníků, které odpovídají tvůrci revize settings.trust_model.collaboratorcommitter.desc=Platné podpisy spolupracovníků tohoto repozitáře budou označeny jako „důvěryhodné“, pokud se shodují s přispěvatelem. V opačném případě budou platné podpisy označeny jako "nedůvěryhodné", pokud se podpis shoduje s přispěvatelem a „neodpovídajícím“ v opačném případě. To přinutí Giteu, aby byla označena jako přispěvatel podepsaných commitů se skutečným přispěvatelem označeným jako Co-Authored-By: a Co-Committed-By: na konci commitu. Výchozí klíč Gitea musí odpovídat uživateli v databázi. @@ -1913,17 +2186,18 @@ settings.delete_notices_2=- Tato operace trvale smaže repozitář <strong>%s</s settings.delete_notices_fork_1=- Rozštěpení repozitáře bude nezávislé po smazání. settings.deletion_success=Repozitář byl odstraněn. settings.update_settings_success=Nastavení repozitáře bylo aktualizováno. +settings.update_settings_no_unit=Repozitář by měl povolit alespoň určitý druh interakce. settings.confirm_delete=Smazat repozitář settings.add_collaborator=Přidat spolupracovníka settings.add_collaborator_success=Spolupracovník byl přidán. settings.add_collaborator_inactive_user=Nelze přidat neaktivního uživatele jako spolupracovníka. settings.add_collaborator_owner=Vlastníka nelze přidat jako spolupracovníka. settings.add_collaborator_duplicate=Spolupracovník je již přidán k tomuto repozitáři. +settings.add_collaborator.blocked_user=Spolupracovník je zablokován vlastníkem repozitáře nebo naopak. settings.delete_collaborator=Odstranit settings.collaborator_deletion=Odstranit spolupracovníka settings.collaborator_deletion_desc=Odstranění spolupracovníka zruší jeho přístup do tohoto repozitáře. Pokračovat? settings.remove_collaborator_success=Spolupracovník byl smazán. -settings.search_user_placeholder=Hledat uživatele… settings.org_not_allowed_to_be_collaborator=Organizace nemůže být přidána jako spolupracovník. settings.change_team_access_not_allowed=Změna přístupu týmu k repozitáře se omezuje na vlastníka organizace settings.team_not_in_organization=Tým není ve stejné organizaci jako repozitář @@ -1931,7 +2205,6 @@ settings.teams=Týmy settings.add_team=Přidat tým settings.add_team_duplicate=Tým již má repozitář settings.add_team_success=Tým má nyní přístup k repozitáři. -settings.search_team=Vyhledat tým… settings.change_team_permission_tip=Oprávnění týmu je nastaveno na stránce nastavení týmu a nelze je změnit pro každý repozitář settings.delete_team_tip=Tento tým má přístup ke všem repositářům a nemůže být odstraněn settings.remove_team_success=Přístup týmu k repozitáři byl odstraněn. @@ -1943,12 +2216,14 @@ settings.webhook_deletion_desc=Odstranění webového háčku smaže jeho nastav settings.webhook_deletion_success=Webový háček byl smazán. settings.webhook.test_delivery=Test doručitelnosti settings.webhook.test_delivery_desc=Vyzkoušet tento webový háček pomocí falešné události. +settings.webhook.test_delivery_desc_disabled=Chcete-li tento webový háček otestovat s falešnou událostí, aktivujte ho. settings.webhook.request=Požadavek settings.webhook.response=Odpověď settings.webhook.headers=Hlavičky settings.webhook.payload=Obsah settings.webhook.body=Tělo zprávy settings.webhook.replay.description=Zopakovat tento webový háček. +settings.webhook.replay.description_disabled=Chcete-li znovu spustit tento webový háček, aktivujte jej. settings.webhook.delivery.success=Událost byla přidána do fronty doručení. Může to trvat několik sekund, než se zobrazí v historii doručení. settings.githooks_desc=Jelikož háčky Gitu jsou spravovány Gitem samotným, můžete upravit soubory háčků k provádění uživatelských operací. settings.githook_edit_desc=Je-li háček neaktivní, bude zobrazen vzorový obsah. Nebude-li zadán žádný obsah, háček bude vypnut. @@ -1995,21 +2270,25 @@ settings.event_issue_milestone=Úkolu přidán milník settings.event_issue_milestone_desc=Úkolu přidán nebo odebrán milník. settings.event_issue_comment=Komentář k úkolu settings.event_issue_comment_desc=Komentář úkolu přidán, upraven nebo smazán. -settings.event_header_pull_request=Události požadavku na natažení -settings.event_pull_request=Požadavek na stažení -settings.event_pull_request_desc=Požadavek na natažení otevřen, uzavřen, znovu otevřen nebo upraven. -settings.event_pull_request_assign=Požadavek na natažení přiřazen -settings.event_pull_request_assign_desc=Požadavek na natažení přiřazen nebo nepřiřazen. -settings.event_pull_request_label=Požadavek na natažení oštítkován -settings.event_pull_request_label_desc=Štítky požadavku na natažení aktualizovány nebo vymazány. -settings.event_pull_request_milestone=Požadavku na natažení přidán milník -settings.event_pull_request_milestone_desc=Požadavku na natažení přidán nebo odebrán milník. -settings.event_pull_request_comment=Požadavek na natažení okomentován -settings.event_pull_request_comment_desc=Komentář požadavku na natažení vytvořen, upraven nebo odstraněn. -settings.event_pull_request_review=Požadavek na natažení přezkoumán -settings.event_pull_request_review_desc=Požadavek na natažení schválen, odmítnut nebo zkontrolován. -settings.event_pull_request_sync=Požadavek na natažení synchronizován -settings.event_pull_request_sync_desc=Požadavek na natažení synchronizován. +settings.event_header_pull_request=Události pull requestu +settings.event_pull_request=Pull request +settings.event_pull_request_desc=Pull request otevřen, uzavřen, znovu otevřen nebo upraven. +settings.event_pull_request_assign=Pull request přiřazen +settings.event_pull_request_assign_desc=Pull request přiřazen nebo nepřiřazen. +settings.event_pull_request_label=Pull request oštítkován +settings.event_pull_request_label_desc=Štítky pull requestu aktualizovány nebo vymazány. +settings.event_pull_request_milestone=Přidán milník pull requestu +settings.event_pull_request_milestone_desc=Přidán nebo odebrán milník pull requestu. +settings.event_pull_request_comment=Pull request okomentován +settings.event_pull_request_comment_desc=Komentář pull requestu vytvořen, upraven nebo odstraněn. +settings.event_pull_request_review=Pull request posouzen +settings.event_pull_request_review_desc=Pull request schválen, odmítnut nebo zkontrolován. +settings.event_pull_request_sync=Pull request synchronizován +settings.event_pull_request_sync_desc=Pull request synchronizován. +settings.event_pull_request_review_request=Požádáno o posouzení pull requestu +settings.event_pull_request_review_request_desc=Přidána nebo ostraněna žádnost o kontrolu pull requestu. +settings.event_pull_request_approvals=Schválení pull requestu +settings.event_pull_request_merge=Sloučení pull requestu settings.event_package=Balíček settings.event_package_desc=Balíček vytvořen nebo odstraněn v repozitáři. settings.branch_filter=Filtr větví @@ -2054,6 +2333,7 @@ settings.title=Název settings.deploy_key_content=Obsah settings.key_been_used=Klíč pro nasazení se stejným obsahem je již používán. settings.key_name_used=Klíč pro nasazení se stejným jménem již existuje. +settings.add_key_success=Klíč pro nasazení „%s“ byl přidán. settings.deploy_key_deletion=Odstranit klíč pro nasazení settings.deploy_key_deletion_desc=Odstranění klíče pro nasazení zruší jeho přístup k repozitáři. Pokračovat? settings.deploy_key_deletion_success=Klíč pro nasazení byl odstraněn. @@ -2071,45 +2351,65 @@ settings.protect_disable_push=Zakázat nahrávání settings.protect_disable_push_desc=Žádné nahrávání do této větve nebude povoleno. settings.protect_enable_push=Povolit nahrávání settings.protect_enable_push_desc=Každý, kdo má přístup k zápisu, bude moci nahrávat do této větve (ale ne vynucená nahrávání). +settings.protect_enable_merge=Povolit sloučení +settings.protect_enable_merge_desc=Každému, kdo má přístup k zápisu, bude povoleno sloučit pull requesty do této větve. settings.protect_whitelist_committers=Povolit nahrání jen vyjmenovaným settings.protect_whitelist_committers_desc=Pouze povolení uživatelé budou moci nahrávat do této větve (ale ne vynucení nahrávání). settings.protect_whitelist_deploy_keys=Povolit nahrání klíčům pro nasazení s přístupem pro zápis. settings.protect_whitelist_users=Povolení uživatelé pro nahrávání: -settings.protect_whitelist_search_users=Hledat uživatele… settings.protect_whitelist_teams=Povolené týmy pro nahrávání: -settings.protect_whitelist_search_teams=Vyhledat týmy… settings.protect_merge_whitelist_committers=Povolit vyjmenovaným slučování -settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným uživatelům nebo týmům slučovat požadavky na natažení do této větve. +settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným uživatelům nebo týmům slučovat pull requesty do této větve. settings.protect_merge_whitelist_users=Povolení uživatelé pro slučování: settings.protect_merge_whitelist_teams=Povolené týmy pro slučování: settings.protect_check_status_contexts=Povolit kontrolu stavu +settings.protect_status_check_patterns=Vzorce kontroly stavu: +settings.protect_status_check_patterns_desc=Zadejte vzory pro určení, které kontroly stavu musí projít před sloučením větví do větve, která odpovídá tomuto pravidlu. Každý řádek určuje vzor. Vzory nemohou být prázdné. settings.protect_check_status_contexts_desc=Požadovat kontrolu stavu před sloučením. Vyberte, jaké kontroly stavu musí projít před tím, než je možné větev sloučit do větve, která vyhovuje tomuto pravidlu. Pokud je povoleno, revize musí být nejprve nahrány do jiné větve, projít kontrolou stavu, a následné sloučeny nebo přímo nahrány do větve, která vyhovuje tomuto pravidlu. Pokud nejsou vybrány žádné kontexty, musí být poslední potvrzení úspěšné bez ohledu na kontext. settings.protect_check_status_contexts_list=Kontroly stavu pro tento repozitář zjištěné během posledního týdne +settings.protect_status_check_matched=Odpovídá +settings.protect_invalid_status_check_pattern=Neplatný vzor kontroly stavu: „%s“. +settings.protect_no_valid_status_check_patterns=Žádné platné vzory kontroly stavu. settings.protect_required_approvals=Požadovaná schválení: -settings.protect_required_approvals_desc=Umožnit sloučení pouze požadavkům na natažení s dostatečným pozitivním hodnocením. +settings.protect_required_approvals_desc=Umožnit sloučení pouze pull requestů s dostatečným pozitivním hodnocením. settings.protect_approvals_whitelist_enabled=Omezit schválení na povolené uživatele nebo týmy settings.protect_approvals_whitelist_enabled_desc=Do požadovaných schválení se započítají pouze posouzení od povolených uživatelů nebo týmů. Bez seznamu povolených se započítává schválení od kohokoli s právem zápisu. settings.protect_approvals_whitelist_users=Povolení posuzovatelé: settings.protect_approvals_whitelist_teams=Povolené týmy pro posuzování: settings.dismiss_stale_approvals=Odmítnout nekvalitní schválení -settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto požadavku na natažení, všechna stará schválení budou zamítnuta. +settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto pull requestu, všechna stará schválení budou zamítnuta. +settings.ignore_stale_approvals=Ignorovat zastaralá schválení +settings.ignore_stale_approvals_desc=Nezapočítávejte schválení, která byla provedena u starších commitů (zastaralých recenzí), do počtu schválení, která má PR. Pokud jsou zastaralá hodnocení již zamítnuta, je to irelevantní. settings.require_signed_commits=Vyžadovat podepsané revize settings.require_signed_commits_desc=Odmítnout nahrání do této větve pokud nejsou podepsaná nebo jsou neověřitelná. +settings.protect_branch_name_pattern=Vzor jména chráněných větví +settings.protect_branch_name_pattern_desc=Vzory jmen chráněných větví. Pro vzorovou syntaxi viz <a href="https://github.com/gobwas/glob">dokumentace</a>. Příklady: main, release/** +settings.protect_patterns=Vzory +settings.protect_protected_file_patterns=Vzory chráněných souborů (oddělené středníkem „;“): +settings.protect_protected_file_patterns_desc=Chráněné soubory, které nemají povoleno být měněny přímo, i když uživatel má právo přidávat, upravovat nebo mazat soubory v této větvi. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentaci pro syntaxi vzoru. Příklady: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. +settings.protect_unprotected_file_patterns=Vzory nechráněných souborů (oddělené středníkem „;“): +settings.protect_unprotected_file_patterns_desc=Nechráněné soubory, které je možné měnit přímo, pokud má uživatel právo zápisu, čímž se obejde omezení push. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentaci pro syntaxi vzoru. Příklady: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. settings.add_protected_branch=Zapnout ochranu settings.delete_protected_branch=Vypnout ochranu +settings.update_protect_branch_success=Ochrana větví pro větev „%s“ byla aktualizována. +settings.remove_protected_branch_success=Ochrana větví pro větev „%s“ byla zakázána. +settings.remove_protected_branch_failed=Odstranění ochranného pravidla větve „%s“ se nezdařilo. settings.protected_branch_deletion=Zakázat ochranu větví settings.protected_branch_deletion_desc=Zakázání ochrany větví umožní uživatelům s právem zápisu nahrávat do této větve. Pokračovat? settings.block_rejected_reviews=Blokovat sloučení při zamítavých posouzeních settings.block_rejected_reviews_desc=Slučování nebude možné, pokud o změny požádají oficiální posuzovatelé, i když je k dispozici dostatek schválení. settings.block_on_official_review_requests=Blokovat sloučení při oficiální žádosti o posouzení settings.block_on_official_review_requests_desc=Slučování nebude možné, pokud mají oficiální požadavek na posouzení, i když mají k dispozici dostatek schválení. -settings.block_outdated_branch=Blokovat sloučení, pokud je požadavek na natažení zastaralý +settings.block_outdated_branch=Blokovat sloučení, pokud je pull request zastaralý settings.block_outdated_branch_desc=Slučování nebude možné, pokud je hlavní větev za základní větví. -settings.default_branch_desc=Vybrat výchozí větev repozitáře pro požadavky na natažení a revize kódu: +settings.default_branch_desc=Vybrat výchozí větev repozitáře pro pull requesty a revize kódu: +settings.merge_style_desc=Sloučit styly settings.default_merge_style_desc=Výchozí styl sloučení pro požadavky na natažení: settings.choose_branch=Vyberte větev… settings.no_protected_branch=Nejsou tu žádné chráněné větve. settings.edit_protected_branch=Upravit +settings.protected_branch_required_rule_name=Požadovaný název pravidla +settings.protected_branch_duplicate_rule_name=Duplikovat název pravidla settings.protected_branch_required_approvals_min=Požadovaná schválení nesmí být záporné číslo. settings.tags=Značky settings.tags.protection=Ochrana značek @@ -2120,18 +2420,27 @@ settings.tags.protection.allowed.teams=Povolené týmy settings.tags.protection.allowed.noone=Nikdo settings.tags.protection.create=Chránit značku settings.tags.protection.none=Neexistují žádné chráněné značky. +settings.tags.protection.pattern.description=Můžete použít jediné jméno nebo vzor glob nebo regulární výraz, který bude odpovídat více značek. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">průvodci chráněnými značkami</a>. settings.bot_token=Token pro robota settings.chat_id=ID chatu +settings.thread_id=ID vlákna settings.matrix.homeserver_url=URL adresa Homeserveru settings.matrix.room_id=ID místnosti settings.matrix.message_type=Typ zprávy settings.archive.button=Archivovat repozitář settings.archive.header=Archivovat tento repozitář +settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo pull requesty. settings.archive.success=Repozitář byl úspěšně archivován. settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů. settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář. settings.archive.branchsettings_unavailable=Nastavení větví není dostupné, pokud je repozitář archivovaný. settings.archive.tagsettings_unavailable=Nastavení značek není k dispozici, pokud je repozitář archivován. +settings.archive.mirrors_unavailable=Zrcadla nejsou k dispozici, pokud je repozitář archivován. +settings.unarchive.button=Obnovit repozitář +settings.unarchive.header=Obnovit tento repozitář +settings.unarchive.text=Obnovení repozitáře vrátí možnost přijímání commitů a nahrávání. Stejně tak se obnoví i možnost zadávání nových úkolů a pull requestů. +settings.unarchive.success=Repozitář byl úspěšně obnoven. +settings.unarchive.error=Nastala chyba při obnovování repozitáře. Prohlédněte si záznam pro více detailů. settings.update_avatar_success=Avatar repozitáře byl aktualizován. settings.lfs=LFS settings.lfs_filelist=LFS soubory uložené v tomto repozitáři @@ -2198,6 +2507,7 @@ diff.show_more=Zobrazit více diff.load=Načíst rozdílové porovnání diff.generated=vygenerováno diff.vendored=vendorováno +diff.comment.add_line_comment=Přidat jednořádkový komentář diff.comment.placeholder=Zanechat komentář diff.comment.markdown_info=Je podporována úprava vzhledu pomocí markdown. diff.comment.add_single_comment=Přidat jeden komentář @@ -2209,7 +2519,9 @@ diff.review.header=Odeslat posouzení diff.review.placeholder=Posoudit komentář diff.review.comment=Okomentovat diff.review.approve=Schválit +diff.review.self_reject=Autoři pull requestu nemohou požadovat změny na svém vlastním pull requestu diff.review.reject=Požadovat změny +diff.review.self_approve=Autoři pull requestu nemohou schválit svůj vlastní pull request diff.committed_by=odevzdal diff.protected=Chráněno diff.image.side_by_side=Vedle sebe @@ -2231,13 +2543,18 @@ release.compare=Porovnat release.edit=upravit release.ahead.commits=<strong>%d</strong> revizí release.ahead.target=do %s od tohoto vydání +tag.ahead.target=do %s od této značky release.source_code=Zdrojový kód release.new_subheader=Vydání organizuje verze projektu. release.edit_subheader=Vydání organizuje verze projektu. release.tag_name=Název značky release.target=Cíl release.tag_helper=Vyberte existující značku nebo vytvořte novou značku. +release.tag_helper_new=Nová značka. Tato značka bude vytvořen z cíle. +release.tag_helper_existing=Stávající značka. release.title=Název vydání +release.title_empty=Název nesmí být prázdný. +release.message=Popište toto vydání release.prerelease_desc=Označit jako předběžná verze release.prerelease_helper=Označit vydání jako nevhodné pro produkční nasazení. release.cancel=Zrušit @@ -2247,6 +2564,7 @@ release.edit_release=Aktualizovat vydání release.delete_release=Smazat vydání release.delete_tag=Smazat značku release.deletion=Smazat vydání +release.deletion_desc=Smazání vydání jej pouze odebere z Gitea. Nebude to mít vliv na značku Git, obsah vašeho repozitáře nebo jeho historii. Pokračovat? release.deletion_success=Vydání bylo odstraněno. release.deletion_tag_desc=Odstraní tuto značku z repozitáře. Obsah repozitáře a historie zůstanou nezměněny. Pokračovat? release.deletion_tag_success=Značka byla odstraněna. @@ -2258,32 +2576,55 @@ release.downloads=Soubory ke stažení release.download_count=Stažení: %s release.add_tag_msg=Použít název a obsah vydání jako zprávu značky. release.add_tag=Vytvořit pouze značku +release.releases_for=Vydání pro %s release.tags_for=Značky pro %s branch.name=Jméno větve +branch.already_exists=Větev pojmenovaná „%s“ již existuje. branch.delete_head=Smazat +branch.delete=Smazat větev „%s“ branch.delete_html=Smazat větev +branch.delete_desc=Smazání větve je trvalé. Přestože zrušená větev může existovat i po krátkou dobu, než bude skutečně odstraněna, NELZE ji většinou vrátit. Pokračovat? +branch.deletion_success=Větev „%s“ byla smazána. +branch.deletion_failed=Nepodařilo se odstranit větev „%s“. +branch.delete_branch_has_new_commits=Větev „%s“ nemůže být smazána, protože byly přidány nové commity po sloučení. branch.create_branch=Vytvořit větev <strong>%s</strong> +branch.create_from=z „%s“ +branch.create_success=Větev „%s“ byla vytvořena. branch.branch_already_exists=Větev „%s“ již existuje v tomto repozitáři. +branch.branch_name_conflict=Jméno větve „%s“ koliduje s již existující větví „%s“. +branch.tag_collision=Větev „%s“ nemůže být vytvořena, protože v repozitáři existuje značka se stejným jménem. branch.deleted_by=Odstranil %s +branch.restore_success=Větev „%s“ byla obnovena. +branch.restore_failed=Nepodařilo se obnovit větev „%s“. +branch.protected_deletion_failed=Větev „%s“ je chráněna. Nemůže být smazána. +branch.default_deletion_failed=Větev „%s“ je výchozí větev. Nelze ji odstranit. +branch.restore=Obnovit větev „%s“ +branch.download=Stáhnout větev „%s“ +branch.rename=Přejmenovat větev „%s“ branch.included_desc=Tato větev je součástí výchozí větve branch.included=Zahrnuje branch.create_new_branch=Vytvořit větev z větve: branch.confirm_create_branch=Vytvořit větev +branch.warning_rename_default_branch=Přejmenováváte výchozí větev. +branch.rename_branch_to=Přejmenovat „%s“ na: branch.confirm_rename_branch=Přejmenovat větev branch.create_branch_operation=Vytvořit větev branch.new_branch=Vytvořit novou větev +branch.new_branch_from=Vytvořit novou větev z „%s“ branch.renamed=Větev %s byla přejmenována na %s. tag.create_tag=Vytvořit značku <strong>%s</strong> tag.create_tag_operation=Vytvořit značku tag.confirm_create_tag=Vytvořit značku +tag.create_tag_from=Vytvořit novou značku z „%s“ tag.create_success=Značka „%s“ byla vytvořena. topic.manage_topics=Spravovat témata topic.done=Hotovo topic.count_prompt=Nelze vybrat více než 25 témat +topic.format_prompt=Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a tečky („.“) a může být dlouhé až 35 znaků. Písmena musí být malá. find_file.go_to_file=Přejít na soubor find_file.no_matching=Nebyl nalezen žádný odpovídající soubor @@ -2291,6 +2632,16 @@ find_file.no_matching=Nebyl nalezen žádný odpovídající soubor error.csv.too_large=Tento soubor nelze vykreslit, protože je příliš velký. error.csv.unexpected=Tento soubor nelze vykreslit, protože obsahuje neočekávaný znak na řádku %d ve sloupci %d. error.csv.invalid_field_count=Soubor nelze vykreslit, protože má nesprávný počet polí na řádku %d. +error.broken_git_hook=Git háčky tohoto repozitáře se zdají být rozbité. Postupujte prosím podle <a target="_blank" rel="noreferrer" href="%s">dokumentace</a>, abyste je opravili, a poté nahrajte nějaké commity pro obnovení stavu. + +[graphs] +component_loading=Načítání %s... +component_loading_failed=Nelze načíst %s +component_loading_info=Může to chvíli trvat… +component_failed_to_load=Došlo k neočekávané chybě. +code_frequency.what=frekvence kódu +contributors.what=příspěvky +recent_commits.what=nedávné commity [org] org_name_holder=Název organizace @@ -2315,23 +2666,28 @@ team_permission_desc=Oprávnění team_unit_desc=Povolit přístup do částí repozitáře team_unit_disabled=(zakázaná) +form.name_reserved=Název organizace „%s“ je rezervován. +form.name_pattern_not_allowed=Vzor „%s“ není povolený v názvu organizace. form.create_org_not_allowed=Nemáte oprávnění vytvářet nové organizace. settings=Nastavení settings.options=Organizace settings.full_name=Celé jméno +settings.email=Kontaktní e-mail settings.website=Webové stránky settings.location=Umístění settings.permission=Oprávnění settings.repoadminchangeteam=Správce úložišť může týmům přidávat a odebírat přístup settings.visibility=Viditelnost settings.visibility.public=Veřejná +settings.visibility.limited=Omezeno (Viditelné pouze pro ověřené uživatele) settings.visibility.limited_shortname=Omezený settings.visibility.private=Soukromá (viditelné jen členům organizace) settings.visibility.private_shortname=Soukromý settings.update_settings=Upravit nastavení settings.update_setting_success=Nastavení organizace bylo upraveno. +settings.change_orgname_prompt=Poznámka: Změna názvu organizace také změní adresu URL vaší organizace a uvolní staré jméno této organizace. settings.change_orgname_redirect_prompt=Staré jméno bude přesměrovávat, dokud nebude znovu obsazeno. settings.update_avatar_success=Avatar organizace byl aktualizován. settings.delete=Smazat organizaci @@ -2391,14 +2747,15 @@ teams.write_permission_desc=Členství v tom týmu poskytuje právo <strong>záp teams.admin_permission_desc=Členství v tom týmu poskytuje právo <strong>správce</strong>: členové mohou číst z, nahrávat do a přidávat spolupracovníky do repozitářů týmu. teams.create_repo_permission_desc=Navíc tento tým uděluje oprávnění <strong>vytvořit repozitář</strong>: členové mohou vytvářet nové repozitáře v organizaci. teams.repositories=Repozitáře týmu -teams.search_repo_placeholder=Hledat repozitář… teams.remove_all_repos_title=Odstranit všechny repozitáře týmu teams.remove_all_repos_desc=Tímto odeberete všechny repozitáře z týmu. teams.add_all_repos_title=Přidat všechny repozitáře teams.add_all_repos_desc=Tímto přidáte do týmu všechny repozitáře organizace. +teams.add_nonexistent_repo=Repositář, který se snažíte přidat, neexistuje. Nejdříve jej vytvořte, prosím. teams.add_duplicate_users=Uživatel je již členem týmu. teams.repos.none=Tento tým nemůže přistoupit k žádným repozitářům. teams.members.none=Žádní členové v tomto týmu. +teams.members.blocked_user=Nelze přidat uživatele, protože je zablokován organizací. teams.specific_repositories=Konkrétní repozitáře teams.specific_repositories_helper=Členové budou mít přístup pouze do repozitářů výslovně přidaných do týmu. Výběrem tohoto <strong>nebudou</strong> automaticky odstraněny již přidané repozitáře pomocí <i>Všechny repozitáře</i>. teams.all_repositories=Všechny repozitáře @@ -2406,39 +2763,48 @@ teams.all_repositories_helper=Tým má přístup ke všem repositářům. Výbě teams.all_repositories_read_permission_desc=Tomuto týmu je udělen přístup pro <strong>Čtení</strong> <strong>všech repozitářů</strong>: členové mohou prohlížet a klonovat repozitáře. teams.all_repositories_write_permission_desc=Tomuto týmu je udělen přístup pro <strong>Zápis</strong> do <strong>všech repozitářů</strong>: členové mohou prohlížet a nahrávat do repozitářů. teams.all_repositories_admin_permission_desc=Tomuto týmu je udělen <strong>Administrátorský</strong> přístup do <strong>všech repozitářů</strong>: členové mohou prohlížet, nahrávat a přidávat spolupracovníky do repozitářů. +teams.invite.title=Byli jste pozváni do týmu <strong>%s</strong> v organizaci <strong>%s</strong>. teams.invite.by=Pozvání od %s teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže. [admin] dashboard=Přehled +self_check=Samokontrola +identity_access=Identita a přístup users=Uživatelské účty organizations=Organizace repositories=Repozitáře hooks=Webové háčky +integrations=Integrace authentication=Zdroje ověření emails=Uživatelské e-maily config=Nastavení +config_summary=Souhrn +config_settings=Nastavení notices=Systémová oznámení monitor=Sledování first_page=První last_page=Poslední total=Celkem: %d +settings=Nastavení správce +dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací. dashboard.statistic=Souhrn -dashboard.operations=Operace údržby dashboard.system_status=Status systému dashboard.operation_name=Název operace dashboard.operation_switch=Přepnout dashboard.operation_run=Spustit dashboard.clean_unbind_oauth=Vyčistit neprovázané OAuth spojení dashboard.clean_unbind_oauth_success=Všechna neprovázaná OAuth spojení byla smazána. -dashboard.task.started=Zahájen úkol: %[1]s -dashboard.task.process=Úkol: %[1]s -dashboard.task.error=Chyba v úkolu: %[1]s: %[3]s -dashboard.task.finished=Úkol: %[1]s začalo %[2]s skončilo -dashboard.task.unknown=Neznámý úkol: %[1]s +dashboard.task.started=Zahájena úloha: %[1]s +dashboard.task.process=Úloha: %[1]s +dashboard.task.cancelled=Úloha: %[1]s zrušean: %[3]s +dashboard.task.error=Chyba v úloze: %[1]s: %[3]s +dashboard.task.finished=Úloha: %[1]s začala %[2]s skončila +dashboard.task.unknown=Neznámá úloha: %[1]s dashboard.cron.started=Začala naplánovaná úloha: %[1]s dashboard.cron.process=Naplánovaná úloha: %[1]s +dashboard.cron.cancelled=Naplánovaná úloha: %[1]s zrušena: %[3]s dashboard.cron.error=Chyba v naplánované úloze: %s: %[3]s dashboard.cron.finished=Naplánovaná úloha: %[1]s skončila dashboard.delete_inactive_accounts=Smazat všechny neaktivované účty @@ -2448,6 +2814,8 @@ dashboard.delete_repo_archives.started=Spuštěna úloha smazání všech archiv dashboard.delete_missing_repos=Smazat všechny repozitáře, které nemají Git soubory dashboard.delete_missing_repos.started=Spuštěna úloha mazání všech repozitářů, které nemají Git soubory. dashboard.delete_generated_repository_avatars=Odstranit vygenerované avatary repozitářů +dashboard.sync_repo_branches=Synchronizovat chybějící větve z git dat do databází +dashboard.sync_repo_tags=Synchronizovat značky z git dat do databáze dashboard.update_mirrors=Aktualizovat zrcadla dashboard.repo_health_check=Kontrola stavu všech repozitářů dashboard.check_repo_stats=Zkontrolovat všechny statistiky repositáře @@ -2462,6 +2830,7 @@ dashboard.reinit_missing_repos=Znovu inicializovat všechny chybějící repozit dashboard.sync_external_users=Synchronizovat externí uživatelská data dashboard.cleanup_hook_task_table=Vyčistit tabulku hook_task dashboard.cleanup_packages=Vyčistit prošlé balíčky +dashboard.cleanup_actions=Vyčištění prošlých záznamů a artefaktů z akcí dashboard.server_uptime=Doba provozu serveru dashboard.current_goroutine=Aktuální Goroutines dashboard.current_memory_usage=Aktuální využití paměti @@ -2495,9 +2864,14 @@ dashboard.delete_old_actions=Odstranit všechny staré akce z databáze dashboard.delete_old_actions.started=Začalo odstraňování všech starých akcí z databáze. dashboard.update_checker=Kontrola aktualizací dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze -dashboard.stop_zombie_tasks=Zastavit zombie úkoly -dashboard.stop_endless_tasks=Zastavit nekonečné úkoly +dashboard.gc_lfs=Úklid LFS meta objektů +dashboard.stop_zombie_tasks=Zastavit zombie úlohy +dashboard.stop_endless_tasks=Zastavit nekonečné úlohy dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy +dashboard.start_schedule_tasks=Spustit naplánované úlohy +dashboard.sync_branch.started=Synchronizace větví byla spuštěna +dashboard.sync_tag.started=Synchronizace značek spuštěna +dashboard.rebuild_issue_indexer=Znovu sestavit index úkolů users.user_manage_panel=Správa uživatelských účtů users.new_account=Vytvořit uživatelský účet @@ -2506,12 +2880,16 @@ users.full_name=Celé jméno users.activated=Aktivován users.admin=Správce users.restricted=Omezený +users.reserved=Rezervováno +users.bot=Bot +users.remote=Vzdálený users.2fa=2FA users.repos=Repozitáře users.created=Vytvořen users.last_login=Poslední přihlášení users.never_login=Nikdy nepřihlášen users.send_register_notify=Odeslat upozornění při registraci uživatele +users.new_success=Uživatelský účet „%s“ byl vytvořen. users.edit=Upravit users.auth_source=Zdroj ověřování users.local=Místní @@ -2536,6 +2914,7 @@ users.still_own_repo=Tento uživatel stále vlastní jeden nebo více repozitá users.still_has_org=Uživatel je člen organizace. Nejprve odstraňte uživatele ze všech organizací. users.purge=Vymazat uživatele users.purge_help=Vynuceně smazat uživatele a všechny repositáře, organizace a balíčky vlastněné uživatelem. Všechny komentáře budou také smazány. +users.still_own_packages=Tento uživatel stále vlastní jeden nebo více balíčků, nejprve odstraňte tyto balíčky. users.deletion_success=Uživatelský účet byl smazán. users.reset_2fa=Resetovat 2FA users.list_status_filter.menu_text=Filtr @@ -2550,6 +2929,7 @@ users.list_status_filter.is_prohibit_login=Zakázat přihlášení users.list_status_filter.not_prohibit_login=Povolit přihlášení users.list_status_filter.is_2fa_enabled=2FA povoleno users.list_status_filter.not_2fa_enabled=2FA zakázáno +users.details=Detaily uživatele emails.email_manage_panel=Správa e-mailů uživatele emails.primary=Hlavní @@ -2562,6 +2942,7 @@ emails.updated=E-mail aktualizován emails.not_updated=Aktualizace požadované e-mailové adresy se nezdařila: %v emails.duplicate_active=Tato e-mailová adresa je již aktivní pro jiného uživatele. emails.change_email_header=Aktualizovat vlastnosti e-mailu +emails.change_email_text=Opravdu chcete aktualizovat tuto e-mailovou adresu? orgs.org_manage_panel=Správa organizací orgs.name=Název @@ -2575,14 +2956,15 @@ repos.unadopted.no_more=Nebyly nalezeny žádné další nepřijaté repositář repos.owner=Vlastník repos.name=Název repos.private=Soukromý -repos.watches=Sledovače -repos.stars=Oblíbení -repos.forks=Rozštěpení repos.issues=Úkoly repos.size=Velikost +repos.lfs_size=Velikost LFS packages.package_manage_panel=Správa balíčků packages.total_size=Celková velikost: %s +packages.unreferenced_size=Neodkazovaná velikost: %s +packages.cleanup=Vyčistit prošlá data +packages.cleanup.success=Úspěšné vyčištění dat, jejichž platnost vypršela packages.owner=Vlastník packages.creator=Tvůrce packages.name=Název @@ -2593,10 +2975,12 @@ packages.size=Velikost packages.published=Publikováno defaulthooks=Výchozí webové háčky +defaulthooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde jsou výchozí a budou zkopírovány do všech nových repozitářů. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">průvodci webovými háčky</a>. defaulthooks.add_webhook=Přidat výchozí webový háček defaulthooks.update_webhook=Aktualizovat výchozí webový háček systemhooks=Systémové webové háčky +systemhooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde budou vykonány na všech repozitářích systému, proto prosím zvažte jakékoli důsledky, které to může mít na výkon. Přečtěte si více v <a target="_blank" rel="noopener" href="https://docs.gitea.io/en-us/webhooks/">průvodci webovými háčky</a>. systemhooks.add_webhook=Přidat systémový webový háček systemhooks.update_webhook=Aktualizovat systémový webový háček @@ -2674,6 +3058,7 @@ auths.oauth2_required_claim_value_helper=Nastavte tuto hodnotu pro omezení při auths.oauth2_group_claim_name=Název tvrzení poskytující názvy skupin pro tento zdroj. (nepovinné) auths.oauth2_admin_group=Hodnota tvrzení pro skupinu uživatelů administrátorů. (Volitelné - vyžaduje název tvrzení výše) auths.oauth2_restricted_group=Hodnota tvrzení pro skupinu omezených uživatelů. (Volitelné - vyžaduje název tvrzení výše) +auths.oauth2_map_group_to_team=Mapa uváděných skupin do organizačních týmů. (Volitelné - vyžaduje výše uvedené jméno) auths.oauth2_map_group_to_team_removal=Odebrat uživatele z synchronizovaných týmů, pokud uživatel nepatří do odpovídající skupiny. auths.enable_auto_register=Povolit zaregistrování se auths.sspi_auto_create_users=Automaticky vytvářet uživatele @@ -2688,21 +3073,24 @@ auths.sspi_default_language=Výchozí jazyk uživatele auths.sspi_default_language_helper=Výchozí jazyk pro uživatele automaticky vytvořené pomocí SSPI auth metody. Pokud dáváte přednost automatickému zjištění jazyka, ponechte prázdné. auths.tips=Tipy auths.tips.oauth2.general=Ověřování OAuth2 +auths.tips.oauth2.general.tip=Při registraci nové OAuth2 autentizace by URL callbacku/přesměrování měla být: auths.tip.oauth2_provider=Poskytovatel OAuth2 auths.tip.bitbucket=Vytvořte nového OAuth konzumenta na https://bitbucket.org/account/user/<vase-uzivatelske-jmeno>/oauth-consumers/new a přidejte oprávnění „Account“ - „Read“ auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci pomocí následujícího menu „Nastavení -> Zabezpečení -> OAuth 2.0 klient“ auths.tip.dropbox=Vytvořte novou aplikaci na https://www.dropbox.com/developers/apps auths.tip.facebook=Registrujte novou aplikaci na https://developers.facebook.com/apps a přidejte produkt „Facebook Login“ auths.tip.github=Registrujte novou OAuth aplikaci na https://github.com/settings/applications/new -auths.tip.gitlab=Registrujte novou aplikaci na https://gitlab.com/profile/applications +auths.tip.gitlab_new=Zaregistrujte novou aplikaci na https://gitlab.com/-/profile/applications auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzole na https://console.developers.google.com/ auths.tip.openid_connect=Použijte OpenID URL pro objevování spojení (<server>/.well-known/openid-configuration) k nastavení koncových bodů auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená auths.tip.discord=Registrujte novou aplikaci na https://discordapp.com/developers/applications/me +auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://docs.gitea.com/development/oauth2-provider auths.tip.yandex=Vytvořte novou aplikaci na https://oauth.yandex.com/client/new. Vyberte následující oprávnění z „Yandex.Passport API“ sekce: „Přístup k e-mailové adrese“, „Přístup k uživatelskému avataru“ a „Přístup k uživatelskému jménu, jménu a příjmení, pohlaví“ auths.tip.mastodon=Vložte vlastní URL instance pro mastodon, kterou se chcete autentizovat (nebo použijte výchozí) auths.edit=Upravit zdroj ověřování auths.activated=Tento zdroj ověřování je aktivován +auths.new_success=Zdroj ověřování „%s“ byl přidán. auths.update_success=Zdroj ověřování byl aktualizován. auths.update=Aktualizovat zdroj ověřování auths.delete=Smazat zdroj ověřování @@ -2710,7 +3098,9 @@ auths.delete_auth_title=Smazat zdroj ověřování auths.delete_auth_desc=Zamezíte přihlášení uživatelům pomocí tohoto zdroje ověřování, pokud ho smažete. Pokračovat? auths.still_in_used=Zdroj ověřování je stále používán. Nejprve převeďte nebo smažte všechny uživatele, kteří používají tento způsob ověřování. auths.deletion_success=Zdroj ověřování byl smazán. +auths.login_source_exist=Zdroj ověřování „%s“ již existuje. auths.login_source_of_type_exist=Zdroj ověřování tohoto typu již existuje. +auths.unable_to_initialize_openid=Nelze inicializovat poskytovatele OpenID Connect: %s auths.invalid_openIdConnectAutoDiscoveryURL=Neplatná URL adresa pro automatické vyhledání (musí být platná adresa URL začínající http:// nebo https://) config.server_config=Nastavení serveru @@ -2725,6 +3115,7 @@ config.disable_router_log=Vypnout log směrovače config.run_user=Spustit jako uživatel config.run_mode=Režim spouštění config.git_version=Verze Gitu +config.app_data_path=Cesta k datům aplikace config.repo_root_path=Kořenový adresář repozitářů config.lfs_root_path=Kořenový adresář LFS config.log_file_root_path=Adresář logů @@ -2799,6 +3190,9 @@ config.mailer_sendmail_timeout=Časový limit Sandmail config.mailer_use_dummy=Fiktivní config.test_email_placeholder=E-mail (např.: test@example.com) config.send_test_mail=Odeslat zkušební e-mail +config.send_test_mail_submit=Odeslat +config.test_mail_failed=Odeslání testovacího e-mailu na „%s“ selhalo: %v +config.test_mail_sent=Zkušební e-mail byl odeslán na „%s“. config.oauth_config=Nastavení ověření OAuth config.oauth_enabled=Zapnutý @@ -2822,6 +3216,7 @@ config.picture_config=Nastavení obrázku a avataru config.picture_service=Služba ikon uživatelů config.disable_gravatar=Zakázat službu Gravatar config.enable_federated_avatar=Povolit avatary z veřejných zdrojů +config.open_with_editor_app_help=Editory "Otevřít" v nabídce klon. Ponecháte-li prázdné, bude použito výchozí. Pro zobrazení výchozího nastavení rozbalte. config.git_config=Konfigurace Gitu config.git_disable_diff_highlight=Zakázat zvýraznění syntaxe v rozdílovém zobrazení @@ -2836,12 +3231,15 @@ config.git_pull_timeout=Časový limit operace stažení config.git_gc_timeout=Časový limit operace GC config.log_config=Nastavení logů +config.logger_name_fmt=Logger: %s config.disabled_logger=Zakázané config.access_log_mode=Režim logování přístupu +config.access_log_template=Šablona záznamu přístupu config.xorm_log_sql=Logovat SQL config.set_setting_failed=Nastavení %s se nezdařilo +monitor.stats=Statistiky monitor.cron=Naplánované úlohy monitor.name=Název @@ -2851,6 +3249,8 @@ monitor.previous=Předešlý čas spuštění monitor.execute_times=Vykonání monitor.process=Spuštěné procesy monitor.stacktrace=Výpisy zásobníku +monitor.processes_count=%d procesů +monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu monitor.desc=Popis monitor.start=Čas zahájení monitor.execute_time=Doba provádění @@ -2868,7 +3268,9 @@ monitor.queue.exemplar=Typ vzoru monitor.queue.numberworkers=Počet workerů monitor.queue.maxnumberworkers=Maximální počet workerů monitor.queue.numberinqueue=Číslo ve frontě +monitor.queue.review_add=Posoudit / přidat workery monitor.queue.settings.title=Nastavení fondu +monitor.queue.settings.desc=Fondy se dynamicky zvětšují v závislosti na blokování jejich pracovních front. monitor.queue.settings.maxnumberworkers=Maximální počet workerů monitor.queue.settings.maxnumberworkers.placeholder=V současné době %[1]d monitor.queue.settings.maxnumberworkers.error=Maximální počet workerů musí být číslo @@ -2892,6 +3294,13 @@ notices.desc=Popis notices.op=Akce notices.delete_success=Systémové upozornění bylo smazáno. +self_check.no_problem_found=Zatím nebyl nalezen žádný problém. +self_check.database_collation_mismatch=Očekávejte, že databáze použije collation: %s +self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Gitea může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání. +self_check.database_inconsistent_collation_columns=Databáze používá collation %s, ale tyto sloupce používají chybné collation. To může způsobit neočekávané problémy. +self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít příkaz "gitea doctor convert", který opraví problémy s collation, nebo můžete také problém vyřešit příkazem "ALTER ... COLLATE ..." SQL ručně. +self_check.database_fix_mssql=Uživatelé MSSQL mohou problém vyřešit pouze pomocí příkazu "ALTER ... COLLATE ..." SQL ručně. + [action] create_repo=vytvořil/a repozitář <a href="%s">%s</a> rename_repo=přejmenoval/a repozitář z <code>%[1]s</code> na <a href="%[2]s">%[3]s</a> @@ -2899,13 +3308,13 @@ commit_repo=nahrál/a do <a href="%[2]s">%[3]s</a> v <a href="%[1]s">%[4]s</a> create_issue=`otevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>` close_issue=`uzavřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>` reopen_issue=`znovuotevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>` -create_pull_request=`vytvořil/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>` -close_pull_request=`uzavřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>` -reopen_pull_request=`znovuotevřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>` +create_pull_request=`vytvořil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` +close_pull_request=`uzavřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` +reopen_pull_request=`znovuotevřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` comment_issue=`okomentoval/a problém <a href="%[1]s">%[3]s#%[2]s</a>` -comment_pull=`okomentoval/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>` -merge_pull_request=`sloučil/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>` -auto_merge_pull_request=`automaticky sloučen požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>` +comment_pull=`okomentoval/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` +merge_pull_request=`sloučil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` +auto_merge_pull_request=`automaticky sloučen pull request <a href="%[1]s">%[3]s#%[2]s</a>` transfer_repo=předal/a repozitář <code>%s</code> uživateli/organizaci <a href="%s">%s</a> push_tag=nahrál/a značku <a href="%[2]s">%[3]s</a> do <a href="%[1]s">%[4]s</a> delete_tag=smazal/a značku %[2]s z <a href="%[1]s">%[3]s</a> @@ -2987,6 +3396,7 @@ desc=Správa balíčků repozitáře. empty=Zatím nejsou žádné balíčky. empty.documentation=Další informace o registru balíčků naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>. empty.repo=Nahráli jste balíček, ale nezobrazil se zde? Přejděte na <a href="%[1]s">nastavení balíčku</a> a propojte jej s tímto repozitářem. +registry.documentation=Další informace o registru %s naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>. filter.type=Typ filter.type.all=Vše filter.no_result=Váš filtr nepřinesl žádné výsledky. @@ -3050,6 +3460,7 @@ debian.repository.distributions=Distribuce debian.repository.components=Komponenty debian.repository.architectures=Architektury generic.download=Stáhnout balíček z příkazové řádky: +go.install=Nainstalujte balíček z příkazové řádky: helm.registry=Nastavte tento registr z příkazového řádku: helm.install=Pro instalaci balíčku spusťte následující příkaz: maven.registry=Nastavte tento registr ve vašem projektu <code>pom.xml</code> souboru: @@ -3071,7 +3482,12 @@ pub.install=Chcete-li nainstalovat balíček pomocí Dart, spusťte následujíc pypi.requires=Vyžaduje Python pypi.install=Pro instalaci balíčku pomocí pip spusťte následující příkaz: rpm.registry=Nastavte tento registr z příkazového řádku: +rpm.distros.redhat=na distribuce založené na RedHat +rpm.distros.suse=na distribuce založené na SUSE rpm.install=Pro instalaci balíčku spusťte následující příkaz: +rpm.repository=Informace o repozitáři +rpm.repository.architectures=Architektury +rpm.repository.multiple_groups=Tento balíček je k dispozici ve více skupinách. rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz: rubygems.install2=nebo ho přidejte do Gemfie: rubygems.dependencies.runtime=Běhové závislosti @@ -3079,6 +3495,8 @@ rubygems.dependencies.development=Vývojové závislosti rubygems.required.ruby=Vyžaduje verzi Ruby rubygems.required.rubygems=Vyžaduje verzi RubyGem swift.registry=Nastavte tento registr z příkazového řádku: +swift.install=Přidejte balíček do svého <code>Package.swift</code> souboru: +swift.install2=a spustit následující příkaz: vagrant.install=Pro přidání Vagrant box spusťte následující příkaz: settings.link=Propojit tento balíček s repozitářem settings.link.description=Pokud propojíte balíček s repozitářem, je tento balíček uveden v seznamu balíčků repozitáře. @@ -3093,14 +3511,17 @@ settings.delete.success=Balíček byl odstraněn. settings.delete.error=Nepodařilo se odstranit balíček. owner.settings.cargo.title=Index registru Cargo owner.settings.cargo.initialize=Inicializovat index +owner.settings.cargo.initialize.description=Pro použití Cargo registru je zapotřebí speciální index Git. Použití této možnosti (znovu)vytvoří repozitář a automaticky jej nastaví. owner.settings.cargo.initialize.error=Nepodařilo se inicializovat Cargo index: %v owner.settings.cargo.initialize.success=Index Cargo byl úspěšně vytvořen. owner.settings.cargo.rebuild=Znovu vytvořit Index +owner.settings.cargo.rebuild.description=Obnova může být užitečná, pokud index není synchronizován s uloženými balíčky Cargo. owner.settings.cargo.rebuild.error=Obnovení Cargo indexu se nezdařilo: %v owner.settings.cargo.rebuild.success=Cargo Index byl úspěšně obnoven. owner.settings.cleanuprules.title=Spravovat pravidla pro čištění owner.settings.cleanuprules.add=Přidat pravidlo pro čištění owner.settings.cleanuprules.edit=Upravit pravidlo pro čištění +owner.settings.cleanuprules.none=Nejsou k dispozici žádná pravidla čištění. Prohlédněte si prosím dokumentaci. owner.settings.cleanuprules.preview=Náhled pravidla pro čištění owner.settings.cleanuprules.preview.overview=%d balíčků má být odstraněno. owner.settings.cleanuprules.preview.none=Pravidlo čištění neodpovídá žádným balíčkům. @@ -3119,6 +3540,7 @@ owner.settings.cleanuprules.success.update=Pravidlo pro čištění bylo aktuali owner.settings.cleanuprules.success.delete=Pravidlo pro čištění bylo odstraněno. owner.settings.chef.title=Registr Chef owner.settings.chef.keypair=Generovat pár klíčů +owner.settings.chef.keypair.description=Pro autentizaci do registru Chef je zapotřebí pár klíčů. Pokud jste předtím vytvořili pár klíčů, nově vygenerovaný pár klíčů vyřadí starý pár klíčů. [secrets] secrets=Tajné klíče @@ -3145,6 +3567,7 @@ status.waiting=Čekání status.running=Probíhá status.success=Úspěch status.failure=Chyba +status.cancelled=Zrušeno status.skipped=Přeskočeno status.blocked=Blokováno @@ -3158,7 +3581,8 @@ runners.description=Popis runners.labels=Štítky runners.last_online=Poslední čas online runners.runner_title=Runner -runners.task_list=Nedávné úkoly na tomto runneru +runners.task_list=Nedávné úlohy na tomto runneru +runners.task_list.no_tasks=Zatím zde nejsou žádné úlohy. runners.task_list.run=Spustit runners.task_list.status=Status runners.task_list.repository=Repozitář @@ -3172,23 +3596,68 @@ runners.delete_runner=Odstranit tento runner runners.delete_runner_success=Runner byl úspěšně odstraněn runners.delete_runner_failed=Odstranění runneru selhalo runners.delete_runner_header=Potvrdit odstranění tohoto runneru +runners.delete_runner_notice=Pokud na tomto runneru běží úloha, bude ukončena a označena jako neúspěšná. Může dojít k přerušení vytváření pracovního postupu. runners.status.unspecified=Neznámý runners.status.idle=Nečinný runners.status.active=Aktivní runners.status.offline=Offline runners.version=Verze +runners.reset_registration_token=Resetovat registrační token +runners.reset_registration_token_success=Registrační token runneru byl úspěšně obnoven runs.all_workflows=Všechny pracovní postupy runs.commit=Commit +runs.scheduled=Naplánováno +runs.pushed_by=náhrán +runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s +runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s +runs.no_job_without_needs=Pracovní postup musí obsahovat alespoň jednu úlohu bez závislostí. +runs.actor=Aktér runs.status=Status +runs.actors_no_select=Všichni aktéři +runs.status_no_select=Všechny stavy +runs.no_results=Nebyly nalezeny žádné výsledky. +runs.no_workflows=Zatím neexistují žádné pracovní postupy. +runs.no_workflows.quick_start=Nevíte jak začít s Gitea Actions? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">průvodce rychlým startem</a>. +runs.no_workflows.documentation=Další informace o Gitea Actions naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>. +runs.no_runs=Pracovní postup zatím nebyl spuštěn. +runs.empty_commit_message=(prázdná zpráva commitu) +workflow.disable=Zakázat pracovní postup +workflow.disable_success=Pracovní postup „%s“ byl úspěšně deaktivován. +workflow.enable=Povolit pracovní postup +workflow.enable_success=Pracovní postup „%s“ byl úspěšně aktivován. +workflow.disabled=Pracovní postup je zakázán. +need_approval_desc=Potřebujete schválení pro spuštění pracovních postupů pro rozštěpený pull request. +variables=Proměnné +variables.management=Správa proměnných +variables.creation=Přidat proměnnou +variables.none=Zatím nejsou žádné proměnné. +variables.deletion=Odstranit proměnnou +variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat? +variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak. +variables.id_not_exist=Proměnná s ID %d neexistuje. +variables.edit=Upravit proměnnou +variables.deletion.failed=Nepodařilo se odstranit proměnnou. +variables.deletion.success=Proměnná byla odstraněna. +variables.creation.failed=Přidání proměnné se nezdařilo. +variables.creation.success=Proměnná „%s“ byla přidána. +variables.update.failed=Úprava proměnné se nezdařila. +variables.update.success=Proměnná byla upravena. [projects] +type-1.display_name=Samostatný projekt +type-2.display_name=Projekt repozitíře type-3.display_name=Projekt organizace [git.filemode] +changed_filemode=%[1]s → %[2]s ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", … +directory=Adresář +normal_file=Normální soubor +executable_file=Spustitelný soubor symbolic_link=Symbolický odkaz +submodule=Submodul diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index c24d25b1ac..9a09c2922e 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -17,6 +17,7 @@ template=Template language=Sprache notifications=Benachrichtigungen active_stopwatch=Aktive Zeiterfassung +tracked_time_summary=Zusammenfassung der erfassten Zeit basierend auf Filtern der Issue-Liste create_new=Erstellen… user_profile_and_more=Profil und Einstellungen… signed_in_as=Angemeldet als @@ -24,6 +25,7 @@ enable_javascript=Diese Website benötigt JavaScript. toc=Inhaltsverzeichnis licenses=Lizenzen return_to_gitea=Zurück zu Gitea +more_items=Weitere Einträge username=Benutzername email=E-Mail-Adresse @@ -90,6 +92,7 @@ remove=Löschen remove_all=Alle entfernen remove_label_str=Element "%s " entfernen edit=Bearbeiten +view=Anzeigen enabled=Aktiviert disabled=Deaktiviert @@ -99,7 +102,7 @@ copy=Kopieren copy_url=URL kopieren copy_hash=Hash kopieren copy_content=Inhalt kopieren -copy_branch=Branchenname kopieren +copy_branch=Branchnamen kopieren copy_success=Kopiert! copy_error=Kopieren fehlgeschlagen copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden @@ -111,6 +114,7 @@ loading=Laden… error=Fehler error404=Die Seite, die Du versuchst aufzurufen, <strong>existiert nicht</strong> oder <strong>Du bist nicht berechtigt</strong>, diese anzusehen. go_back=Zurück +invalid_data=Ungültige Daten: %v never=Niemals unknown=Unbekannt @@ -121,6 +125,7 @@ pin=Anheften unpin=Loslösen artifacts=Artefakte +confirm_delete_artifact=Bist du sicher, dass du das Artefakt '%s' löschen möchtest? archived=Archiviert @@ -139,6 +144,43 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen? name=Name value=Wert +filter=Filter +filter.clear=Filter leeren +filter.is_archived=Archiviert +filter.not_archived=Nicht archiviert +filter.is_fork=Fork +filter.not_fork=Kein Fork +filter.is_mirror=Gespiegelt +filter.not_mirror=Nicht gespiegelt +filter.is_template=Template +filter.not_template=Kein Template +filter.public=Öffentlich +filter.private=Privat + +no_results_found=Es wurden keine Ergebnisse gefunden. + +[search] +search=Suche ... +type_tooltip=Suchmodus +fuzzy=Ähnlich +fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind +match=Genau +match_tooltip=Nur genau zum Suchbegriff passende Ergebnisse einbeziehen +repo_kind=Repositories durchsuchen ... +user_kind=Benutzer durchsuchen ... +org_kind=Organisationen durchsuchen ... +team_kind=Teams durchsuchen ... +code_kind=Code durchsuchen ... +code_search_unavailable=Zurzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. +code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" bereitgestellt. Es könnte bessere Ergebnisse geben, wenn der Website-Administrator den Repository-Indexer aktiviert. +package_kind=Pakete durchsuchen ... +project_kind=Projekte durchsuchen ... +branch_kind=Branches durchsuchen ... +commit_kind=Commits durchsuchen ... +runner_kind=Runner durchsuchen ... +no_results=Es wurden keine passenden Ergebnisse gefunden. +keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. + [aria] navbar=Navigationsleiste footer=Fußzeile @@ -244,6 +286,7 @@ email_title=E-Mail-Einstellungen smtp_addr=SMTP-Host smtp_port=SMTP-Port smtp_from=E-Mail senden als +smtp_from_invalid=Die „E-Mail senden als“ Adresse ist ungültig smtp_from_helper=E-Mail-Adresse, die von Gitea genutzt werden soll. Bitte gib die E-Mail-Adresse im Format „"Name" <email@example.com>“ ein. mailer_user=SMTP-Benutzername mailer_password=SMTP-Passwort @@ -303,6 +346,7 @@ env_config_keys=Umgebungskonfiguration env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet: [home] +nav_menu=Navigationsmenü uname_holder=E-Mail-Adresse oder Benutzername password_holder=Passwort switch_dashboard_context=Kontext der Übersichtsseite wechseln @@ -312,7 +356,6 @@ collaborative_repos=Gemeinschaftliche Repositories my_orgs=Meine Organisationen my_mirrors=Meine Mirrors view_home=%s ansehen -search_repos=Finde ein Repository… filter=Andere Filter filter_by_team_repositories=Nach Team-Repositories filtern feed_of=`Feed von "%s"` @@ -333,20 +376,8 @@ issues.in_your_repos=Eigene Repositories repos=Repositories users=Benutzer organizations=Organisationen -search=Suche go_to=Gehe zu code=Code -search.type.tooltip=Suchmodus -search.fuzzy=Ähnlich -search.fuzzy.tooltip=Zeige auch Ergebnisse, die dem Suchbegriff ähneln -search.match=Genau -search.match.tooltip=Zeige nur Ergebnisse, die exakt mit dem Suchbegriff übereinstimmen -code_search_unavailable=Derzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. -repo_no_results=Keine passenden Repositories gefunden. -user_no_results=Keine passenden Benutzer gefunden. -org_no_results=Keine passenden Organisationen gefunden. -code_no_results=Es konnte kein passender Code für deinen Suchbegriff gefunden werden. -code_search_results=`Suchergebnisse für "%s"` code_last_indexed_at=Zuletzt indexiert %s relevant_repositories_tooltip=Repositories, die Forks sind oder die kein Thema, kein Symbol und keine Beschreibung haben, werden ausgeblendet. relevant_repositories=Es werden nur relevante Repositories angezeigt, <a href="%s">ungefilterte Ergebnisse anzeigen</a>. @@ -359,11 +390,12 @@ disable_register_prompt=Die Registrierung ist deaktiviert. Bitte wende dich an d disable_register_mail=E-Mail-Bestätigung bei der Registrierung ist deaktiviert. manual_activation_only=Kontaktiere den Website-Administrator, um die Aktivierung abzuschließen. remember_me=Dieses Gerät speichern +remember_me.compromised=Das Login-Token ist nicht mehr gültig, was auf ein kompromittiertes Konto hindeuten kann. Bitte überprüfe dein Konto auf ungewöhnliche Aktivitäten. forgot_password_title=Passwort vergessen forgot_password=Passwort vergessen? sign_up_now=Noch kein Konto? Jetzt registrieren. sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen! -confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die Registrierung abzuschließen. +confirmation_mail_sent_prompt_ex=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>gesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern. must_change_password=Aktualisiere dein Passwort allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen) reset_password_mail_sent_prompt=Eine Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb von %s, um den Wiederherstellungsprozess abzuschließen. @@ -373,6 +405,7 @@ prohibit_login=Anmelden verboten prohibit_login_desc=Die Anmeldung mit diesem Konto ist nicht gestattet. Bitte kontaktiere den Administrator. resent_limit_prompt=Du hast bereits eine Aktivierungs-E-Mail angefordert. Bitte warte 3 Minuten und probiere es dann nochmal. has_unconfirmed_mail=Hallo %s, du hast eine unbestätigte E-Mail-Adresse (<b>%s</b>). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue senden möchtest, klicke bitte auf den folgenden Button. +change_unconfirmed_mail_address=Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du sie hier ändern und eine neue Bestätigungs-E-Mail senden. resend_mail=Aktivierungs-E-Mail erneut verschicken email_not_associate=Diese E-Mail-Adresse ist mit keinem Konto verknüpft. send_reset_mail=Wiederherstellungs-E-Mail senden @@ -420,6 +453,7 @@ authorization_failed_desc=Die Autorisierung ist fehlgeschlagen, da wir eine ung sspi_auth_failed=SSPI-Authentifizierung fehlgeschlagen password_pwned=Das von dir gewählte Passwort befindet sich auf einer <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">List gestohlener Passwörter</a>, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern. password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden +last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben. [mail] view_it_on=Auf %s ansehen @@ -552,6 +586,7 @@ team_name_been_taken=Der Teamname ist bereits vergeben. team_no_units_error=Das Team muss auf mindestens einen Bereich Zugriff haben. email_been_used=Die E-Mail-Adresse wird bereits verwendet. email_invalid=Die E-Mail-Adresse ist ungültig. +email_domain_is_not_allowed=Die Domain der Benutzer-E-Mail <b>%s</b> steht im Widerspruch zu EMAIL_DOMAIN_ALLOWLIST oder EMAIL_DOMAIN_BLOCKLIST. Bitte stelle sicher, dass deine Operation erwartet ist. openid_been_used=Die OpenID-Adresse "%s" wird bereits verwendet. username_password_incorrect=Benutzername oder Passwort ist falsch. password_complexity=Das Passwort erfüllt nicht die Komplexitätsanforderungen: @@ -563,6 +598,8 @@ enterred_invalid_repo_name=Der eingegebenen Repository-Name ist falsch. enterred_invalid_org_name=Der eingegebene Organisation-Name ist falsch. enterred_invalid_owner_name=Der Name des neuen Besitzers ist ungültig. enterred_invalid_password=Das eingegebene Passwort ist falsch. +unset_password=Der Login-Benutzer hat das Passwort nicht gesetzt. +unsupported_login_type=Der Anmeldetyp wird zum Löschen des Kontos nicht unterstützt. user_not_exist=Dieser Benutzer ist nicht vorhanden. team_not_exist=Dieses Team existiert nicht. last_org_owner=Du kannst den letzten Benutzer nicht aus dem 'Besitzer'-Team entfernen. Es muss mindestens einen Besitzer in einer Organisation geben. @@ -585,6 +622,8 @@ org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete, target_branch_not_exist=Der Ziel-Branch existiert nicht. +admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte. + [user] change_avatar=Profilbild ändern… joined_on=Beigetreten am %s @@ -610,6 +649,30 @@ form.name_reserved=Der Benutzername "%s" ist reserviert. form.name_pattern_not_allowed=Das Muster "%s" ist nicht in einem Benutzernamen erlaubt. form.name_chars_not_allowed=Benutzername "%s" enthält ungültige Zeichen. +block.block=Sperren +block.block.user=Benutzer sperren +block.block.org=Benutzer für Organisation sperren +block.block.failure=Fehler beim Sperren des Benutzers: %s +block.unblock=Entsperren +block.unblock.failure=Fehler beim Entsperren des Benutzers: %s +block.blocked=Du hast diesen Benutzer gesperrt. +block.title=Einen Benutzer sperren +block.info=Das Blockieren eines Benutzers hindert ihn daran, mit Repositories zu interagieren, wie zum Beispiel das Öffnen oder Kommentieren von Pull Requests oder Issues. Erfahre mehr über das Blockieren eines Benutzers. +block.info_1=Das Blockieren eines Benutzers verhindert folgende Aktionen auf deinem Konto und deinen Repositories: +block.info_2=deinem Konto folgen +block.info_3=dir Benachrichtigungen durch @Erwähnung deines Benutzernamens senden +block.info_4=dich als Mitarbeiter in deren Repositories einladen +block.info_5=Repositories favorisieren, forken oder beobachten +block.info_6=Issues oder Pull Requests öffnen und kommentieren +block.info_7=auf deine Kommentare in Issues oder Pull Requests reagieren +block.user_to_block=Zu sperrender Benutzer +block.note=Anmerkung +block.note.title=Optionale Anmerkung: +block.note.info=Die Anmerkung ist für den blockierten Benutzer nicht sichtbar. +block.note.edit=Anmerkung bearbeiten +block.list=Gesperrte Benutzer +block.list.none=Du hast noch keine Benutzer gesperrt. + [settings] profile=Profil account=Account @@ -754,7 +817,6 @@ gpg_invalid_token_signature=Der GPG Key, die Signatur, und das Token stimmen nic gpg_token_required=Du musst eine Signatur für das folgende Token angeben gpg_token=Token gpg_token_help=Du kannst eine Signatur wie folgt generieren: -gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=GPG Textsignatur (armored signature) key_signature_gpg_placeholder=Beginnt mit '-----BEGIN PGP SIGNATURE-----' verify_gpg_key_success=GPG-Schlüssel "%s" wurde verifiziert. @@ -862,6 +924,7 @@ revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die A revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen. twofa_desc=Zwei-Faktor-Authentifizierung trägt zu einer höheren Accountsicherheit bei. +twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen. twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung <strong>eingeschaltet</strong>. twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet. twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren @@ -884,6 +947,8 @@ webauthn_register_key=Sicherheitsschlüssel hinzufügen webauthn_nickname=Nickname webauthn_delete_key=Sicherheitsschlüssel entfernen webauthn_delete_key_desc=Wenn du einen Sicherheitsschlüssel entfernst, kannst du dich nicht mehr mit ihm anmelden. Fortfahren? +webauthn_key_loss_warning=Wenn du deine Sicherheitsschlüssel verlierst, verlierst du den Zugriff auf dein Konto. +webauthn_alternative_tip=Möglicherweise möchtest du eine zusätzliche Authentifizierungsmethode konfigurieren. manage_account_links=Verknüpfte Accounts verwalten manage_account_links_desc=Diese externen Accounts sind mit deinem Gitea-Account verknüpft. @@ -920,6 +985,7 @@ visibility.private=Privat visibility.private_tooltip=Sichtbar nur für Mitglieder von Organisationen, denen du beigetreten bist [repo] +new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des Änderungsverlaufs. Schon woanders vorhanden? <a href="%s">Migration eines Repositorys.</a> owner=Besitzer owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist. repo_name=Repository-Name @@ -943,8 +1009,9 @@ fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht fork_branch=Branch, der zum Fork geklont werden soll all_branches=Alle Branches fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind. +fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest. use_template=Dieses Template verwenden -clone_in_vsc=In VS Code klonen +open_with_editor=Mit %s öffnen download_zip=ZIP herunterladen download_tar=TAR.GZ herunterladen download_bundle=BUNDLE herunterladen @@ -960,6 +1027,8 @@ issue_labels_helper=Wähle ein Issue-Label-Set. license=Lizenz license_helper=Wähle eine Lizenz aus. license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">eine Lizenz wählen</a>. +object_format=Objektformat +object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel. readme=README readme_helper=Wähle eine README-Vorlage aus. readme_helper_desc=Hier kannst du eine komplette Beschreibung für dein Projekt schreiben. @@ -977,6 +1046,7 @@ mirror_prune=Entfernen mirror_prune_desc=Entferne veraltete remote-tracking Referenzen mirror_interval=Mirror-Intervall (gültige Zeiteinheiten sind 'h', 'm', 's'). 0 deaktiviert die regelmäßige Synchronisation. (Minimales Intervall: %s) mirror_interval_invalid=Das Spiegel-Intervall ist ungültig. +mirror_sync=synchronisiert mirror_sync_on_commit=Synchronisieren, wenn Commits gepusht wurden mirror_address=Klonen via URL mirror_address_desc=Gib alle erforderlichen Anmeldedaten im Abschnitt "Authentifizierung" ein. @@ -994,6 +1064,7 @@ watchers=Beobachter stargazers=Favorisiert von stars_remove_warning=Dies wird alle Sterne aus diesem Repository entfernen. forks=Forks +stars=Favoriten reactions_more=und %d weitere unit_disabled=Der Administrator hat diesen Repository-Bereich deaktiviert. language_other=Andere @@ -1027,6 +1098,7 @@ desc.public=Öffentlich desc.template=Template desc.internal=Intern desc.archived=Archiviert +desc.sha256=SHA256 template.items=Template-Elemente template.git_content=Git Inhalt (Standardbranch) @@ -1114,6 +1186,7 @@ watch=Beobachten unstar=Favorit entfernen star=Favorisieren fork=Fork +action.blocked_user=Die Aktion kann nicht ausgeführt werden, da du vom Repository-Eigentümer blockiert wurdest. download_archive=Repository herunterladen more_operations=Weitere Operationen @@ -1177,6 +1250,8 @@ audio_not_supported_in_browser=Dein Browser unterstützt den HTML5 'audio'-Tag n stored_lfs=Gespeichert mit Git LFS symbolic_link=Softlink executable_file=Ausführbare Datei +vendored=Vendor +generated=Generiert commit_graph=Commit graph commit_graph.select=Branches auswählen commit_graph.hide_pr_refs=Pull-Requests ausblenden @@ -1240,6 +1315,8 @@ editor.file_editing_no_longer_exists=Die bearbeitete Datei "%s" existiert nicht editor.file_deleting_no_longer_exists=Die zu löschende Datei "%s" existiert nicht mehr in diesem Repository. editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. <a target="_blank" rel="noopener noreferrer" href="%s">Hier klicken</a>, um die Änderungen anzusehen, oder <strong>Änderungen erneut comitten</strong>, um sie zu überschreiben. editor.file_already_exists=Eine Datei mit dem Namen '%s' existiert bereits in diesem Repository. +editor.commit_id_not_matching=Die Commit-ID stimmt nicht mit der ID überein, bei welcher du mit der Bearbeitung begonnen hast. Commite in einen Patch-Branch und merge daraufhin. +editor.push_out_of_date=Der Push scheint veraltet zu sein. editor.commit_empty_file_header=Leere Datei committen editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren? editor.no_changes_to_show=Keine Änderungen vorhanden. @@ -1263,9 +1340,8 @@ commits.desc=Durchsuche die Quellcode-Änderungshistorie. commits.commits=Commits commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien. commits.nothing_to_compare=Diese Branches sind auf demselben Stand. -commits.search=Commits durchsuchen… commits.search.tooltip=Du kannst Suchbegriffen "author:", " committer:", "after:", oder " before:" voranstellen, z.B. "revert author:Alice before:2019-04-01". -commits.find=Suchen +commits.search_branch=Dieser Branch commits.search_all=Alle Branches commits.author=Autor commits.message=Nachricht @@ -1316,7 +1392,6 @@ projects.type.basic_kanban=Einfaches Kanban projects.type.bug_triage=Bug Triage projects.template.desc=Projektvorlage projects.template.desc_helper=Wähle eine Projektvorlage aus, um loszulegen -projects.type.uncategorized=Nicht kategorisiert projects.column.edit=Spalte bearbeiten projects.column.edit_title=Name projects.column.new_title=Name @@ -1324,10 +1399,8 @@ projects.column.new_submit=Spalte erstellen projects.column.new=Neue Spalte projects.column.set_default=Als Standard verwenden projects.column.set_default_desc=Diese Spalte als Standard für unkategorisierte Issues und Pull Requests festlegen -projects.column.unset_default=Standard entfernen -projects.column.unset_default_desc=Diese Spalte als Standard entfernen projects.column.delete=Spalte löschen -projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues nach 'Nicht kategorisiert' verschoben. Fortfahren? +projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle Einträge in die Standard-Spalte verschoben. Fortfahren? projects.column.color=Farbe projects.open=Öffnen projects.close=Schließen @@ -1362,6 +1435,8 @@ issues.new.assignees=Zuständig issues.new.clear_assignees=Zuständige entfernen issues.new.no_assignees=Niemand zuständig issues.new.no_reviewers=Keine Reviewer +issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. +issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.choose.get_started=Los geht's issues.choose.open_external_link=Öffnen issues.choose.blank=Standard @@ -1439,7 +1514,6 @@ issues.filter_sort.moststars=Meiste Favoriten issues.filter_sort.feweststars=Wenigste Favoriten issues.filter_sort.mostforks=Meiste Forks issues.filter_sort.fewestforks=Wenigste Forks -issues.keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. issues.action_open=Öffnen issues.action_close=Schließen issues.action_label=Label @@ -1477,6 +1551,7 @@ issues.close_comment_issue=Kommentieren und schließen issues.reopen_issue=Wieder öffnen issues.reopen_comment_issue=Kommentieren und wieder öffnen issues.create_comment=Kommentieren +issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen` issues.reopened_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet` issues.commit_ref_at=`hat dieses Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> aus einem Commit referenziert` @@ -1675,6 +1750,7 @@ compare.compare_head=vergleichen pulls.desc=Pull-Requests und Code-Reviews aktivieren. pulls.new=Neuer Pull-Request +pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. pulls.view=Pull-Request ansehen pulls.compare_changes=Neuer Pull-Request pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben @@ -1691,7 +1767,6 @@ pulls.compare_compare=pullen von pulls.switch_comparison_type=Vergleichstyp wechseln pulls.switch_head_and_base=Head und Base vertauschen pulls.filter_branch=Branch filtern -pulls.no_results=Keine Ergebnisse verfügbar. pulls.show_all_commits=Alle Commits anzeigen pulls.show_changes_since_your_last_review=Zeige Änderungen seit deinem letzten Review pulls.showing_only_single_commit=Nur Änderungen aus Commit %[1]s werden angezeigt @@ -1700,6 +1775,7 @@ pulls.select_commit_hold_shift_for_range=Commit auswählen. Halte Shift + klicke pulls.review_only_possible_for_full_diff=Ein Review ist nur möglich, wenn das vollständige Diff angezeigt wird pulls.filter_changes_by_commit=Nach Commit filtern pulls.nothing_to_compare=Diese Branches sind identisch. Es muss kein Pull-Request erstellt werden. +pulls.nothing_to_compare_have_tag=Der ausgewählte Branch und Tag sind gleich. pulls.nothing_to_compare_and_allow_empty_pr=Diese Branches sind gleich. Der Pull-Request wird leer sein. pulls.has_pull_request=`Es existiert bereits ein Pull-Request zwischen diesen beiden Branches: <a href="%[1]s">%[2]s#%[3]d</a>` pulls.create=Pull-Request erstellen @@ -1758,6 +1834,7 @@ pulls.merge_pull_request=Merge Commit erstellen pulls.rebase_merge_pull_request=Rebasen und dann fast-forwarden pulls.rebase_merge_commit_pull_request=Rebasen und dann mergen pulls.squash_merge_pull_request=Squash Commit erstellen +pulls.fast_forward_only_merge_pull_request=Nur Fast-forward pulls.merge_manually=Manuell mergen pulls.merge_commit_id=Der Mergecommit ID pulls.require_signed_wont_sign=Der Branch erfordert einen signierten Commit, aber dieser Merge wird nicht signiert @@ -1782,6 +1859,8 @@ pulls.status_checks_failure=Einige Prüfungen sind fehlgeschlagen pulls.status_checks_error=Einige Checks meldeten Fehler pulls.status_checks_requested=Erforderlich pulls.status_checks_details=Details +pulls.status_checks_hide_all=Alle Prüfungen ausblenden +pulls.status_checks_show_all=Alle Prüfungen anzeigen pulls.update_branch=Branch durch Mergen aktualisieren pulls.update_branch_rebase=Branch durch Rebase aktualisieren pulls.update_branch_success=Branch-Aktualisierung erfolgreich @@ -1790,6 +1869,11 @@ pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits pulls.close=Pull-Request schließen pulls.closed_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen` pulls.reopened_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet` +pulls.cmd_instruction_hint=`Zeige <a class="show-instruction">Kommandozeilenanweisungen</a>.` +pulls.cmd_instruction_checkout_title=Checkout +pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen. +pulls.cmd_instruction_merge_title=Mergen +pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren. pulls.clear_merge_message=Merge-Nachricht löschen pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten. @@ -1887,6 +1971,10 @@ wiki.page_name_desc=Gib einen Namen für diese Wiki-Seite ein. Spezielle Namen s wiki.original_git_entry_tooltip=Originale Git-Datei anstatt eines benutzerfreundlichen Links anzeigen. activity=Aktivität +activity.navbar.pulse=Puls +activity.navbar.code_frequency=Code-Frequenz +activity.navbar.contributors=Mitwirkende +activity.navbar.recent_commits=Neueste Commits activity.period.filter_label=Zeitraum: activity.period.daily=1 Tag activity.period.halfweekly=3 Tage @@ -1952,16 +2040,10 @@ activity.git_stats_and_deletions=und activity.git_stats_deletion_1=%d Löschung activity.git_stats_deletion_n=%d Löschungen -search=Suchen -search.search_repo=Repository durchsuchen -search.type.tooltip=Suchmodus -search.fuzzy=Ähnlich -search.fuzzy.tooltip=Zeige auch Ergebnisse, die dem Suchbegriff ähneln -search.match=Genau -search.match.tooltip=Zeige nur Ergebnisse, die exakt mit dem Suchbegriff übereinstimmen -search.results=Suchergebnisse für „%s“ in <a href="%s"> %s</a> -search.code_no_results=Es konnte kein passender Code für deinen Suchbegriff gefunden werden. -search.code_search_unavailable=Derzeit ist die Code-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. +contributors.contribution_type.filter_label=Beitragstyp: +contributors.contribution_type.commits=Commits +contributors.contribution_type.additions=Ergänzungen +contributors.contribution_type.deletions=Löschungen settings=Einstellungen settings.desc=In den Einstellungen kannst du die Einstellungen des Repositories anpassen @@ -1989,6 +2071,7 @@ settings.mirror_settings.docs.doc_link_title=Wie spiegele ich Repositories? settings.mirror_settings.docs.doc_link_pull_section=den Abschnitt "Von einem entfernten Repository pullen" in der Dokumentation. settings.mirror_settings.docs.pulling_remote_title=Aus einem Remote-Repository pullen settings.mirror_settings.mirrored_repository=Gespiegeltes Repository +settings.mirror_settings.pushed_repository=Gepushtes Repository settings.mirror_settings.direction=Richtung settings.mirror_settings.direction.pull=Pull settings.mirror_settings.direction.push=Push @@ -2010,6 +2093,8 @@ settings.branches.add_new_rule=Neue Regel hinzufügen settings.advanced_settings=Erweiterte Einstellungen settings.wiki_desc=Repository-Wiki aktivieren settings.use_internal_wiki=Eingebautes Wiki verwenden +settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch +settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen. settings.use_external_wiki=Externes Wiki verwenden settings.external_wiki_url=Externe Wiki-URL settings.external_wiki_url_error=Die externe Wiki-URL ist ungültig. @@ -2040,6 +2125,10 @@ settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern settings.releases_desc=Repository-Releases aktivieren settings.packages_desc=Repository Packages Registry aktivieren settings.projects_desc=Repository-Projekte aktivieren +settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen) +settings.projects_mode_repo=Nur Repo-Projekte +settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte +settings.projects_mode_all=Alle Projekte settings.actions_desc=Repository-Actions aktivieren settings.admin_settings=Administratoreinstellungen settings.admin_enable_health_check=Repository-Health-Checks aktivieren (git fsck) @@ -2065,6 +2154,7 @@ settings.convert_fork_succeed=Der Fork wurde in ein normales Repository konverti settings.transfer=Besitz übertragen settings.transfer.rejected=Repository-Übertragung wurde abgelehnt. settings.transfer.success=Repository-Übertragung war erfolgreich. +settings.transfer.blocked_user=Das Repository kann nicht übertragen werden, da du vom Repository-Eigentümer blockiert wurdest. settings.transfer_abort=Übertragung abbrechen settings.transfer_abort_invalid=Du kannst nur eingeleitete Repository-Übertragung abbrechen. settings.transfer_abort_success=Die Repository-Übertragung zu %s wurde abgebrochen. @@ -2110,11 +2200,11 @@ settings.add_collaborator_success=Der Mitarbeiter wurde hinzugefügt. settings.add_collaborator_inactive_user=Inaktive Benutzer können nicht als Mitarbeiter hinzufügt werden. settings.add_collaborator_owner=Besitzer können nicht als Mitarbeiter hinzugefügt werden. settings.add_collaborator_duplicate=Der Mitarbeiter ist bereits zu diesem Repository hinzugefügt. +settings.add_collaborator.blocked_user=Der Mitwirkende wurde vom Eigentümer des Repositories blockiert oder umgekehrt. settings.delete_collaborator=Entfernen settings.collaborator_deletion=Mitarbeiter entfernen settings.collaborator_deletion_desc=Nach dem Löschen wird dieser Mitarbeiter keinen Zugriff mehr auf dieses Repository haben. Fortfahren? settings.remove_collaborator_success=Der Mitarbeiter wurde entfernt. -settings.search_user_placeholder=Benutzer suchen… settings.org_not_allowed_to_be_collaborator=Organisationen können nicht als Mitarbeiter hinzugefügt werden. settings.change_team_access_not_allowed=Nur der Besitzer der Organisation kann die Zugangsrechte des Teams ändern settings.team_not_in_organization=Das Team ist nicht in der gleichen Organisation wie das Repository @@ -2122,7 +2212,6 @@ settings.teams=Teams settings.add_team=Team hinzufügen settings.add_team_duplicate=Das Team ist dem Repository schon zugeordnet settings.add_team_success=Das Team hat nun Zugriff auf das Repository. -settings.search_team=Team suchen… settings.change_team_permission_tip=Die Team-Berechtigung ist auf der Team-Einstellungsseite festgelegt und kann nicht für ein Repository geändert werden settings.delete_team_tip=Dieses Team hat Zugriff auf alle Repositories und kann nicht entfernt werden settings.remove_team_success=Der Zugriff des Teams auf das Repository wurde zurückgezogen. @@ -2275,9 +2364,7 @@ settings.protect_whitelist_committers=Schütze gewhitelistete Commiter settings.protect_whitelist_committers_desc=Jeder, der auf der Whitelist steht, darf in diesen Branch pushen (aber kein Force-Push). settings.protect_whitelist_deploy_keys=Deploy-Schlüssel mit Schreibzugriff zum Pushen whitelisten. settings.protect_whitelist_users=Nutzer, die pushen dürfen: -settings.protect_whitelist_search_users=Benutzer suchen… settings.protect_whitelist_teams=Teams, die pushen dürfen: -settings.protect_whitelist_search_teams=Teams suchen… settings.protect_merge_whitelist_committers=Merge-Whitelist aktivieren settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Pull-Requests in diesen Branch zu mergen. settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen: @@ -2298,9 +2385,12 @@ settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer: settings.protect_approvals_whitelist_teams=Freigeschaltete Teams: settings.dismiss_stale_approvals=Entferne alte Genehmigungen settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt. +settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren +settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits erteilt wurden (veraltete Genehmigungen), nicht bei der Anzahl an Genehmigungen mitzählen. Irrelevant, falls veraltete Genehmigungen bereits verworfen wurden. settings.require_signed_commits=Signierte Commits erforderlich settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind. settings.protect_branch_name_pattern=Muster für geschützte Branchnamen +settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe <a href="https://github.com/gobwas/glob">die Dokumentation</a> für die Muster-Syntax. Beispiele: main, release/** settings.protect_patterns=Muster settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt): settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> Dokumentation zur Mustersyntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. @@ -2352,6 +2442,7 @@ settings.archive.error=Beim Versuch, das Repository zu archivieren, ist ein Fehl settings.archive.error_ismirror=Du kannst keinen Repo-Mirror archivieren. settings.archive.branchsettings_unavailable=Branch-Einstellungen sind nicht verfügbar wenn das Repo archiviert ist. settings.archive.tagsettings_unavailable=Tag Einstellungen sind nicht verfügbar, wenn das Repo archiviert wurde. +settings.archive.mirrors_unavailable=Mirrors sind nicht verfügbar, wenn das Repository archiviert ist. settings.unarchive.button=Archivieren rückgängig machen settings.unarchive.header=Archivieren dieses Repositories rückgängig machen settings.unarchive.text=Durch das Aufheben der Archivierung kann das Repo wieder Commits und Pushes sowie neue Issues und Pull-Requests empfangen. @@ -2518,7 +2609,6 @@ branch.default_deletion_failed=Branch "%s" kann nicht gelöscht werden, da diese branch.restore=Branch "%s" wiederherstellen branch.download=Branch "%s" herunterladen branch.rename=Branch "%s" umbenennen -branch.search=Branch suchen branch.included_desc=Dieser Branch ist im Standard-Branch enthalten branch.included=Enthalten branch.create_new_branch=Branch aus Branch erstellen: @@ -2549,6 +2639,16 @@ find_file.no_matching=Keine passende Datei gefunden error.csv.too_large=Diese Datei kann nicht gerendert werden, da sie zu groß ist. error.csv.unexpected=Diese Datei kann nicht gerendert werden, da sie ein unerwartetes Zeichen in Zeile %d und Spalte %d enthält. error.csv.invalid_field_count=Diese Datei kann nicht gerendert werden, da sie eine falsche Anzahl an Feldern in Zeile %d hat. +error.broken_git_hook=Git-Hooks dieses Repositories scheinen defekt zu sein. Bitte folge der <a target="_blank" rel="noreferrer" href="%s">Dokumentation</a>, um dies zu beheben, pushe dann ein paar Commits und den Status zu aktualisieren. + +[graphs] +component_loading=%s werden geladen ... +component_loading_failed=%s konnten nicht geladen werden +component_loading_info=Dies kann ein wenig dauern … +component_failed_to_load=Ein unerwarteter Fehler ist aufgetreten. +code_frequency.what=Code-Frequenz +contributors.what=Beiträge +recent_commits.what=Neueste Commits [org] org_name_holder=Name der Organisation @@ -2654,7 +2754,6 @@ teams.write_permission_desc=Dieses Team hat <strong>Schreibzugriff</strong>: Mit teams.admin_permission_desc=Dieses Team hat <strong>Adminzugriff</strong>: Mitglieder dieses Teams können Team-Repositories ansehen, auf sie pushen und Mitarbeiter hinzufügen. teams.create_repo_permission_desc=Zusätzlich erteilt dieses Team die Berechtigung <strong>Repository erstellen</strong>: Mitglieder können neue Repositories in der Organisation erstellen. teams.repositories=Team-Repositories -teams.search_repo_placeholder=Repository durchsuchen… teams.remove_all_repos_title=Alle Team-Repositories entfernen teams.remove_all_repos_desc=Dies entfernt alle Repositories von dem Team. teams.add_all_repos_title=Alle Repositories hinzufügen @@ -2663,6 +2762,7 @@ teams.add_nonexistent_repo=Das Repository, das du hinzufügen möchtest, existie teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied. teams.repos.none=Dieses Team hat Zugang zu keinem Repository. teams.members.none=Keine Mitglieder in diesem Team. +teams.members.blocked_user=Der Benutzer kann nicht hinzugefügt werden, da er von der Organisation blockiert wurde. teams.specific_repositories=Bestimmte Repositories teams.specific_repositories_helper=Mitglieder haben nur Zugriff auf Repositories, die explizit dem Team hinzugefügt wurden. Wenn Du diese Option wählst, werden Repositories, die bereits mit <i>Alle Repositories</i> hinzugefügt wurden, <strong>nicht</strong> automatisch entfernt. teams.all_repositories=Alle Repositories @@ -2676,6 +2776,7 @@ teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Tea [admin] dashboard=Dashboard +self_check=Selbstprüfung identity_access=Identität & Zugriff users=Benutzerkonten organizations=Organisationen @@ -2686,6 +2787,8 @@ integrations=Integrationen authentication=Authentifizierungsquellen emails=Benutzer E-Mails config=Konfiguration +config_summary=Übersicht +config_settings=Einstellungen notices=Systemmitteilungen monitor=Monitoring first_page=Erste @@ -2695,7 +2798,6 @@ settings=Administratoreinstellungen dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">Blog</a>. dashboard.statistic=Übersicht -dashboard.operations=Wartungsoperationen dashboard.system_status=System-Status dashboard.operation_name=Name der Operation dashboard.operation_switch=Wechseln @@ -2721,6 +2823,7 @@ dashboard.delete_missing_repos=Alle Repository-Datensätze mit verloren gegangen dashboard.delete_missing_repos.started=Alle Repositories löschen, die den Git-File-Task nicht gestartet haben. dashboard.delete_generated_repository_avatars=Generierte Repository-Avatare löschen dashboard.sync_repo_branches=Fehlende Branches aus den Git-Daten in die Datenbank synchronisieren +dashboard.sync_repo_tags=Tags von Git-Daten in die Datenbank synchronisieren dashboard.update_mirrors=Mirrors aktualisieren dashboard.repo_health_check=Healthchecks für alle Repositories ausführen dashboard.check_repo_stats=Überprüfe alle Repository-Statistiken @@ -2775,6 +2878,7 @@ dashboard.stop_endless_tasks=Endlose Aufgaben stoppen dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen dashboard.start_schedule_tasks=Terminierte Aufgaben starten dashboard.sync_branch.started=Synchronisierung der Branches gestartet +dashboard.sync_tag.started=Tag-Synchronisierung gestartet dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen users.user_manage_panel=Benutzerkontenverwaltung @@ -2846,6 +2950,7 @@ emails.updated=E-Mail aktualisiert emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: %v emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet. emails.change_email_header=E-Mail-Eigenschaften aktualisieren +emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest? orgs.org_manage_panel=Organisationsverwaltung orgs.name=Name @@ -2859,9 +2964,6 @@ repos.unadopted.no_more=Keine weiteren nicht übernommenen Repositories gefunden repos.owner=Besitzer repos.name=Name repos.private=Privat -repos.watches=Beobachtungen -repos.stars=Favoriten -repos.forks=Forks repos.issues=Issues repos.size=Größe repos.lfs_size=LFS-Größe @@ -2870,6 +2972,7 @@ packages.package_manage_panel=Paketverwaltung packages.total_size=Gesamtgröße: %s packages.unreferenced_size=Nicht referenzierte Größe: %s packages.cleanup=Veraltete Daten löschen +packages.cleanup.success=Abgelaufene Daten erfolgreich bereinigt packages.owner=Besitzer packages.creator=Ersteller packages.name=Name @@ -2985,7 +3088,7 @@ auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 cli auths.tip.dropbox=Erstelle eine neue App auf https://www.dropbox.com/developers/apps. auths.tip.facebook=Erstelle eine neue Anwendung auf https://developers.facebook.com/apps und füge das Produkt „Facebook Login“ hinzu. auths.tip.github=Erstelle unter https://github.com/settings/applications/new eine neue OAuth-Anwendung. -auths.tip.gitlab=Erstelle unter https://gitlab.com/profile/applications eine neue Anwendung. +auths.tip.gitlab_new=Erstelle eine neue Anwendung unter https://gitlab.com/-/profile/applications auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter https://console.developers.google.com/ auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL (<server>/.well-known/openid-configuration), um die Endpunkte zu spezifizieren auths.tip.twitter=Gehe auf https://dev.twitter.com/apps, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist @@ -3121,6 +3224,7 @@ config.picture_config=Bild-und-Profilbild-Konfiguration config.picture_service=Bilderservice config.disable_gravatar=Gravatar deaktivieren config.enable_federated_avatar=Föderierte Profilbilder einschalten +config.open_with_editor_app_help=Die „Öffnen mit“-Editoren für das Klon-Menü. Falls leer, wird die Standardeinstellung verwendet. Erweitern, um die Standardeinstellung zu sehen. config.git_config=Git-Konfiguration config.git_disable_diff_highlight=Diff-Syntaxhervorhebung ausschalten @@ -3199,6 +3303,13 @@ notices.desc=Beschreibung notices.op=Aktion notices.delete_success=Diese Systemmeldung wurde gelöscht. +self_check.no_problem_found=Bisher wurde kein Problem festgestellt. +self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s +self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren. +self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen. +self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben. +self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden. + [action] create_repo=hat das Repository <a href="%s">%s</a> erstellt rename_repo=hat das Repository von <code>%[1]s</code> zu <a href="%[2]s">%[3]s</a> umbenannt @@ -3383,6 +3494,9 @@ rpm.registry=Diese Registry über die Kommandozeile einrichten: rpm.distros.redhat=auf RedHat-basierten Distributionen rpm.distros.suse=auf SUSE-basierten Distributionen rpm.install=Nutze folgenden Befehl, um das Paket zu installieren: +rpm.repository=Repository-Informationen +rpm.repository.architectures=Architekturen +rpm.repository.multiple_groups=Dieses Paket ist in mehreren Gruppen verfügbar. rubygems.install=Um das Paket mit gem zu installieren, führe den folgenden Befehl aus: rubygems.install2=oder füg es zum Gemfile hinzu: rubygems.dependencies.runtime=Laufzeitabhängigkeiten @@ -3508,12 +3622,18 @@ runs.commit=Commit runs.scheduled=Geplant runs.pushed_by=gepusht von runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s +runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s +runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten. runs.actor=Initiator runs.status=Status runs.actors_no_select=Alle Initiatoren runs.status_no_select=Alle Status runs.no_results=Keine passenden Ergebnisse gefunden. +runs.no_workflows=Es gibt noch keine Workflows. +runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">die Schnellstart-Anleitung</a>. +runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der <a target="_blank" rel="noopener noreferrer" href="%s"> Dokumentation</a>. runs.no_runs=Der Workflow hat noch keine Ausführungen. +runs.empty_commit_message=(leere Commit-Nachricht) workflow.disable=Workflow deaktivieren workflow.disable_success=Workflow '%s' erfolgreich deaktiviert. @@ -3530,7 +3650,7 @@ variables.none=Es gibt noch keine Variablen. variables.deletion=Variable entfernen variables.deletion.description=Das Entfernen einer Variable ist dauerhaft und kann nicht rückgängig gemacht werden. Fortfahren? variables.description=Variablen werden an bestimmte Aktionen übergeben und können nicht anderweitig gelesen werden. -variables.id_not_exist=Variable mit ID %d existiert nicht. +variables.id_not_exist=Eine Variable mit ID %d existiert nicht. variables.edit=Variable bearbeiten variables.deletion.failed=Fehler beim Entfernen der Variable. variables.deletion.success=Die Variable wurde entfernt. diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 2424ee3fb6..6ce5ae1ce9 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -4,6 +4,7 @@ explore=Εξερεύνηση help=Βοήθεια logo=Λογότυπο sign_in=Είσοδος +sign_in_with_provider=Είσοδος με %s sign_in_or=ή sign_out=Έξοδος sign_up=Εγγραφή @@ -16,6 +17,7 @@ template=Πρότυπο language=Γλώσσα notifications=Ειδοποιήσεις active_stopwatch=Ενεργή Καταγραφή Χρόνου +tracked_time_summary=Περίληψη του χρόνου παρακολούθησης με βάση τα φίλτρα της λίστας ζητημάτων create_new=Δημιουργία… user_profile_and_more=Προφίλ και ρυθμίσεις… signed_in_as=Είσοδος ως @@ -79,6 +81,7 @@ milestones=Ορόσημα ok=OK cancel=Ακύρωση +retry=Επανάληψη rerun=Επανεκτέλεση rerun_all=Επανεκτέλεση όλων save=Αποθήκευση @@ -88,12 +91,15 @@ remove=Αφαίρεση remove_all=Αφαίρεση Όλων remove_label_str=`Αφαίρεση του αντικειμένου "%s"` edit=Επεξεργασία +view=Προβολή enabled=Ενεργοποιημένο disabled=Απενεργοποιημένο +locked=Κλειδωμένο copy=Αντιγραφή copy_url=Αντιγραφή URL +copy_hash=Αντιγραφή hash copy_content=Αντιγραφή περιεχομένου copy_branch=Αντιγραφή ονόματος κλάδου copy_success=Αντιγράφηκε! @@ -106,6 +112,7 @@ loading=Φόρτωση… error=Σφάλμα error404=Η σελίδα που προσπαθείτε να φτάσετε είτε <strong>δεν υπάρχει</strong> είτε <strong>δεν είστε εξουσιοδοτημένοι</strong> για να την δείτε. +go_back=Επιστροφή never=Ποτέ unknown=Άγνωστη @@ -127,11 +134,22 @@ concept_user_organization=Οργανισμός show_timestamps=Εμφάνιση χρονοσημάνσεων show_log_seconds=Εμφάνιση δευτερολέπτων show_full_screen=Εμφάνιση πλήρους οθόνης +download_logs=Λήψη καταγραφών +confirm_delete_selected=Επιβεβαιώνετε τη διαγραφή όλων των επιλεγμένων στοιχείων; name=Όνομα value=Τιμή +filter=Φίλτρο +filter.is_archived=Αρχειοθετήθηκε +filter.is_template=Πρότυπο +filter.public=Δημόσιος +filter.private=Ιδιωτικό + + +[search] + [aria] navbar=Γραμμή Πλοήγησης footer=Υποσέλιδο @@ -166,6 +184,7 @@ string.desc=Z - A [error] occurred=Παρουσιάστηκε ένα σφάλμα +report_message=Αν πιστεύετε ότι αυτό είναι ένα πρόβλημα στο Gitea, παρακαλούμε αναζητήστε ζητήματα στο <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a> ή ανοίξτε ένα νέο ζήτημα εάν είναι απαραίτητο. missing_csrf=Bad Request: δεν υπάρχει διακριτικό CSRF invalid_csrf=Λάθος Αίτημα: μη έγκυρο διακριτικό CSRF not_found=Ο προορισμός δεν βρέθηκε. @@ -174,6 +193,7 @@ network_error=Σφάλμα δικτύου [startpage] app_desc=Μια ανώδυνη, αυτο-φιλοξενούμενη υπηρεσία Git install=Εύκολο στην εγκατάσταση +install_desc=Απλά <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">εκτελέστε το αρχείο προγράμματος</a> για την πλατφόρμα σας, χρήσιμοποιήστε το με το <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, ή εγκαταστήστε το <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">πακέτο</a>. platform=Πολυπλατφορμικό platform_desc=Ο Gitea τρέχει οπουδήποτε <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> μπορεί να γίνει compile για: Windows, macOS, Linux, ARM, κλπ. Επιλέξτε αυτό που αγαπάτε! lightweight=Ελαφρύ @@ -218,6 +238,7 @@ repo_path_helper=Τα απομακρυσμένα αποθετήρια Git θα lfs_path=Ριζική Διαδρομή Git LFS lfs_path_helper=Τα αρχεία που παρακολουθούνται από το Git LFS θα αποθηκεύονται σε αυτόν τον φάκελο. Αφήστε κενό για να το απενεργοποιήσετε. run_user=Εκτέλεση Σαν Χρήστη +run_user_helper=Το όνομα του χρήστη του λειτουργικού συστήματος ο οποίος εκτελεί το Gitea. Επισημαίνεται ότι αυτός ο χρήστης πρέπει να έχει πρόσβαση στο ριζικό φάκελο του αποθετηρίου. domain=Domain Διακομιστή domain_helper=Όνομα domain διακομιστή ή η διεύθυνση του. ssh_port=Θύρα της υπηρεσίας SSH @@ -290,6 +311,7 @@ password_algorithm_helper=Ορίστε τον αλγόριθμο κατακερ enable_update_checker=Ενεργοποίηση Ελεγκτή Ενημερώσεων enable_update_checker_helper=Ελέγχει περιοδικά για νέες εκδόσεις κάνοντας σύνδεση στο gitea.io. env_config_keys=Ρυθμίσεις Περιβάλλοντος +env_config_keys_prompt=Οι ακόλουθες μεταβλητές περιβάλλοντος θα εφαρμοστούν επίσης στο αρχείο ρυθμίσεων σας: [home] uname_holder=Όνομα Χρήστη ή Διεύθυνση Email @@ -301,7 +323,6 @@ collaborative_repos=Συνεργατικά Αποθετήρια my_orgs=Οι Οργανισμοί Μου my_mirrors=Τα Αντίγραφα Μου view_home=Προβολή %s -search_repos=Βρείτε ένα αποθετήριο… filter=Άλλα Φίλτρα filter_by_team_repositories=Φιλτράρισμα ανά αποθετήρια ομάδας feed_of=`Τροφοδοσία του "%s"` @@ -322,20 +343,8 @@ issues.in_your_repos=Στα αποθετήρια σας repos=Αποθετήρια users=Χρήστες organizations=Οργανισμοί -search=Αναζήτηση go_to=Μετάβαση σε code=Κώδικας -search.type.tooltip=Τύπος αναζήτησης -search.fuzzy=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=Τα αποθετήρια που είναι forks ή που δεν έχουν θέμα, εικονίδιο και περιγραφή είναι κρυμμένα. relevant_repositories=Εμφανίζονται μόνο τα σχετικά αποθετήρια, <a href="%s">εμφάνιση χωρίς φίλτρο</a>. @@ -348,16 +357,18 @@ disable_register_prompt=Η εγγραφή είναι απενεργοποιημ disable_register_mail=Η Επιβεβαίωση email για την εγγραφή είναι απενεργοποιημένη. manual_activation_only=Επικοινωνήστε με το διαχειριστή της υπηρεσίας για να ολοκληρώσετε την ενεργοποίηση. remember_me=Απομνημόνευση αυτής της συσκευής +remember_me.compromised=Το διακριτικό σύνδεσης δεν είναι πλέον έγκυρο, αυτό ίσως υποδεικνύει έναν κλεμμένο λογαριασμό. Παρακαλώ ελέγξτε το λογαριασμό σας για ασυνήθιστες δραστηριότητες. forgot_password_title=Ξέχασα Τον Κωδικό Πρόσβασης forgot_password=Ξεχάσατε τον κωδικό πρόσβασης; sign_up_now=Χρειάζεστε λογαριασμό; Εγγραφείτε τώρα. -confirmation_mail_sent_prompt=Ένα νέο email επιβεβαίωσης έχει σταλεί στο <b>%s</b>. Παρακαλώ ελέγξτε τα εισερχόμενα σας μέσα στις επόμενες %s για να ολοκληρώσετε τη διαδικασία εγγραφής. +sign_up_successful=Ο λογαριασμός δημιουργήθηκε επιτυχώς. Καλώς ορίσατε! must_change_password=Ενημερώστε τον κωδικό πρόσβασης σας allow_password_change=Απαιτείται από το χρήστη να αλλάξει τον κωδικό πρόσβασης (συνιστόμενο) reset_password_mail_sent_prompt=Ένα email επιβεβαίωσης έχει σταλεί στο <b>%s</b>. Παρακαλώ ελέγξτε τα εισερχόμενα σας στις επόμενες %s για να ολοκληρώσετε τη διαδικασία ανάκτησης λογαριασμού. active_your_account=Ενεργοποιήστε Το Λογαριασμό Σας account_activated=Ο λογαριασμός έχει ενεργοποιηθεί prohibit_login=Απαγορεύεται η Σύνδεση +prohibit_login_desc=Ο λογαριασμός σας δεν επιτρέπεται να συνδεθεί, παρακαλούμε επικοινωνήστε με το διαχειριστή σας. resent_limit_prompt=Έχετε ήδη ζητήσει ένα email ενεργοποίησης πρόσφατα. Παρακαλώ περιμένετε 3 λεπτά και προσπαθήστε ξανά. has_unconfirmed_mail=Γεια σας %s, έχετε μια ανεπιβεβαίωτη διεύθυνση ηλεκτρονικού ταχυδρομείου (<b>%s</b>). Εάν δεν έχετε λάβει email επιβεβαίωσης ή χρειάζεται να αποστείλετε εκ νέου ένα νέο, παρακαλώ κάντε κλικ στο παρακάτω κουμπί. resend_mail=Κάντε κλικ εδώ για να στείλετε ξανά το email ενεργοποίησης @@ -365,8 +376,10 @@ email_not_associate=Η διεύθυνση ηλεκτρονικού ταχυδρ send_reset_mail=Αποστολή Email Ανάκτησης Λογαριασμού reset_password=Ανάκτηση Λογαριασμού invalid_code=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έχει λήξει. +invalid_code_forgot_password=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έληξε. Πατήστε <a href="%s">εδώ</a> για να ξεκινήσετε νέα συνεδρία. invalid_password=Ο κωδικός πρόσβασης σας δεν ταιριάζει με τον κωδικό που χρησιμοποιήθηκε για τη δημιουργία του λογαριασμού. reset_password_helper=Ανάκτηση Λογαριασμού +reset_password_wrong_user=Έχετε συνδεθεί ως %s, αλλά ο σύνδεσμος ανάκτησης λογαριασμού προορίζεται για το %s password_too_short=Το μήκος του κωδικού πρόσβασης δεν μπορεί να είναι μικρότερο από %d χαρακτήρες. non_local_account=Οι μη τοπικοί χρήστες δεν μπορούν να ενημερώσουν τον κωδικό πρόσβασής τους μέσω του διεπαφής web του Gitea. verify=Επαλήθευση @@ -391,6 +404,7 @@ openid_connect_title=Σύνδεση σε υπάρχων λογαριασμό openid_connect_desc=Το επιλεγμένο OpenID URI είναι άγνωστο. Συνδέστε το με ένα νέο λογαριασμό εδώ. openid_register_title=Δημιουργία νέου λογαριασμού openid_register_desc=Το επιλεγμένο OpenID URI είναι άγνωστο. Συνδέστε το με ένα νέο λογαριασμό εδώ. +openid_signin_desc=Εισάγετε το OpenID URI σας. Για παράδειγμα: alice.openid.example.org ή https://openid.example.org/alice. disable_forgot_password_mail=Η ανάκτηση λογαριασμού είναι απενεργοποιημένη επειδή δεν έχει οριστεί email. Παρακαλούμε επικοινωνήστε με το διαχειριστή. disable_forgot_password_mail_admin=Η ανάκτηση λογαριασμού είναι διαθέσιμη μόνο όταν έχει οριστεί το email. Παρακαλούμε ορίστει το email σας για να ενεργοποιήσετε την ανάκτηση λογαριασμού. email_domain_blacklisted=Δεν μπορείτε να εγγραφείτε με τη διεύθυνση email σας. @@ -400,7 +414,9 @@ authorize_application_created_by=Αυτή η εφαρμογή δημιουργή authorize_application_description=Εάν παραχωρήσετε την πρόσβαση, θα μπορεί να έχει πρόσβαση και να γράφει σε όλες τις πληροφορίες του λογαριασμού σας, συμπεριλαμβανομένων των ιδιωτικών αποθετηρίων και οργανισμών. authorize_title=Εξουσιοδότηση του "%s" για έχει πρόσβαση στο λογαριασμό σας; authorization_failed=Αποτυχία εξουσιοδότησης +authorization_failed_desc=Η εξουσιοδότηση απέτυχε επειδή εντοπίστηκε μια μη έγκυρη αίτηση. Παρακαλούμε επικοινωνήστε με το συντηρητή της εφαρμογής που προσπαθήσατε να εξουσιοδοτήσετε. sspi_auth_failed=Αποτυχία ταυτοποίησης SSPI +password_pwned=Ο κωδικός πρόσβασης που επιλέξατε είναι σε μια λίστα <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">κλεμμένων κωδικών πρόσβασης</a> που προηγουμένως εκτέθηκαν σε παραβίαση δημόσιων δεδομένων. Παρακαλώ δοκιμάστε ξανά με διαφορετικό κωδικό πρόσβασης και σκεφτείτε να αλλάξετε αυτόν τον κωδικό πρόσβασης όπου αλλού χρησιμοποιείται. password_pwned_err=Δεν ήταν δυνατή η ολοκλήρωση του αιτήματος προς το HaveIBeenPwned [mail] @@ -415,6 +431,7 @@ activate_account.text_1=Γεια σας <b>%[1]s</b>, ευχαριστούμε activate_account.text_2=Παρακαλούμε κάντε κλικ στον παρακάτω σύνδεσμο για να ενεργοποιήσετε το λογαριασμό σας μέσα σε <b>%s</b>: activate_email=Επιβεβαιώστε τη διεύθυνση email σας +activate_email.title=%s, παρακαλώ επαληθεύστε τη διεύθυνση email σας activate_email.text=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για να επαληθεύσετε τη διεύθυνση email σας στο <b>%s</b>: register_notify=Καλώς ήλθατε στο Gitea @@ -566,6 +583,7 @@ org_still_own_packages=Αυτός ο οργανισμός κατέχει ακό target_branch_not_exist=Ο κλάδος προορισμού δεν υπάρχει. + [user] change_avatar=Αλλαγή του avatar σας… joined_on=Εγγράφηκε την %s @@ -584,11 +602,14 @@ user_bio=Βιογραφικό disabled_public_activity=Αυτός ο χρήστης έχει απενεργοποιήσει τη δημόσια προβολή της δραστηριότητας. email_visibility.limited=Η διεύθυνση email σας είναι ορατή σε όλους τους ταυτοποιημένους χρήστες email_visibility.private=Η διεύθυνση email σας είναι ορατή μόνο σε εσάς και στους διαχειριστές +show_on_map=Εμφάνιση της τοποθεσίας στο χάρτη +settings=Ρυθμίσεις Χρήστη form.name_reserved=Το όνομα χρήστη "%s" είναι δεσμευμένο. form.name_pattern_not_allowed=Το μοτίβο "%s" δεν επιτρέπεται μέσα σε ένα όνομα χρήστη. form.name_chars_not_allowed=Το όνομα χρήστη "%s" περιέχει μη έγκυρους χαρακτήρες. + [settings] profile=Προφίλ account=Λογαριασμός @@ -605,9 +626,13 @@ delete=Διαγραφή Λογαριασμού twofa=Έλεγχος Ταυτότητας Δύο Παραγόντων account_link=Συνδεδεμένοι Λογαριασμοί organization=Οργανισμοί +uid=UID webauthn=Κλειδιά Ασφαλείας public_profile=Δημόσιο Προφίλ +biography_placeholder=Πείτε μας λίγο για τον εαυτό σας! (Μπορείτε να γράψετε με Markdown) +location_placeholder=Μοιραστείτε την κατά προσέγγιση τοποθεσία σας με άλλους +profile_desc=Ελέγξτε πώς εμφανίζεται το προφίλ σας σε άλλους χρήστες. Η κύρια διεύθυνση email σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση κωδικού πρόσβασης και λειτουργίες Git που βασίζονται στο web. password_username_disabled=Οι μη τοπικοί χρήστες δεν επιτρέπεται να αλλάξουν το όνομα χρήστη τους. Επικοινωνήστε με το διαχειριστή σας για περισσότερες λεπτομέρειες. full_name=Πλήρες Όνομα website=Ιστοσελίδα @@ -619,6 +644,8 @@ update_language_not_found=Η γλώσσα "%s" δεν είναι διαθέσι update_language_success=Η γλώσσα ενημερώθηκε. update_profile_success=Το προφίλ σας έχει ενημερωθεί. change_username=Το όνομα χρήστη σας έχει αλλάξει. +change_username_prompt=Σημείωση: Αλλάζοντας το όνομα χρήστη σας αλλάζει επίσης το URL του λογαριασμού σας. +change_username_redirect_prompt=Το παλιό όνομα χρήστη θα ανακατευθύνει μέχρι να ζητηθεί ξανά. continue=Συνέχεια cancel=Ακύρωση language=Γλώσσα @@ -643,6 +670,7 @@ comment_type_group_project=Έργο comment_type_group_issue_ref=Αναφορά ζητήματος saved_successfully=Οι ρυθμίσεις σας αποθηκεύτηκαν επιτυχώς. privacy=Απόρρητο +keep_activity_private=Απόκρυψη Δραστηριότητας από τη σελίδα προφίλ keep_activity_private_popup=Με αυτή την επιλογή η δραστηριότητα σας είναι ορατή μόνο σε εσάς και τους διαχειριστές lookup_avatar_by_mail=Αναζήτηση ενός Avatar με διεύθυνση email @@ -652,12 +680,14 @@ choose_new_avatar=Επιλέξτε νέα εικόνα update_avatar=Ενημέρωση Εικόνας delete_current_avatar=Διαγραφή Τρέχουσας Εικόνας uploaded_avatar_not_a_image=Το αρχείο που ανεβάσατε δεν είναι εικόνα. +uploaded_avatar_is_too_big=Το μέγεθος αρχείου που ανέβηκε (%d KiB) υπερβαίνει το μέγιστο μέγεθος (%d KiB). update_avatar_success=Η εικόνα σας έχει ενημερωθεί. update_user_avatar_success=Το avatar του χρήστη ενημερώθηκε. change_password=Ενημέρωση Κωδικού Πρόσβασης old_password=Τρέχων Κωδικός Πρόσβασης new_password=Νέος Κωδικός Πρόσβασης +retype_new_password=Επιβεβαίωση Νέου Κωδικού Πρόσβασης password_incorrect=Ο τρέχων κωδικός πρόσβασης είναι λάθος. change_password_success=Ο κωδικός πρόσβασής σας έχει ενημερωθεί. Από εδώ και τώρα συνδέεστε χρησιμοποιώντας τον νέο κωδικό πρόσβασής σας. password_change_disabled=Οι μη τοπικοί χρήστες δεν μπορούν να ενημερώσουν τον κωδικό πρόσβασής τους μέσω του διεπαφής web του Gitea. @@ -666,6 +696,7 @@ emails=Διευθύνσεις Email manage_emails=Διαχείριση Διευθύνσεων Email manage_themes=Επιλέξτε προεπιλεγμένο θέμα διεπαφής manage_openid=Διαχείριση Διευθύνσεων OpenID +email_desc=Η κύρια διεύθυνση ηλεκτρονικού ταχυδρομείου σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση του κωδικού πρόσβασης και, εφόσον δεν είναι κρυμμένη, λειτουργίες Git στον ιστότοπο. theme_desc=Αυτό θα είναι το προεπιλεγμένο θέμα διεπαφής σας σε όλη την ιστοσελίδα. primary=Κύριο activated=Ενεργό @@ -673,6 +704,7 @@ requires_activation=Απαιτείται ενεργοποίηση primary_email=Αλλαγή κυριότητας activate_email=Αποστολή Ενεργοποίησης activations_pending=Εκκρεμούν Ενεργοποιήσεις +can_not_add_email_activations_pending=Εκκρεμεί μια ενεργοποίηση, δοκιμάστε ξανά σε λίγα λεπτά αν θέλετε να προσθέσετε ένα νέο email. delete_email=Αφαίρεση email_deletion=Αφαίρεση Διεύθυνσης Email email_deletion_desc=Η διεύθυνση ηλεκτρονικού ταχυδρομείου και οι σχετικές πληροφορίες θα αφαιρεθούν από τον λογαριασμό σας. Οι υποβολές Git από αυτή τη διεύθυνση email θα παραμείνουν αμετάβλητες. Συνέχεια; @@ -691,6 +723,7 @@ add_email_success=Η νέα διεύθυνση email έχει προστεθεί email_preference_set_success=Οι προτιμήσεις email έχουν οριστεί επιτυχώς. add_openid_success=Προστέθηκε η νέα διεύθυνση OpenID. keep_email_private=Απόκρυψη Διεύθυνσης Email +keep_email_private_popup=Αυτό θα κρύψει τη διεύθυνση ηλεκτρονικού ταχυδρομείου σας από το προφίλ σας, καθώς και όταν κάνετε ένα pull request ή επεξεργαστείτε ένα αρχείο χρησιμοποιώντας τη διεπαφή ιστού. Οι ωθούμενες υποβολές δεν θα τροποποιηθούν. Χρησιμοποιήστε το %s στις υποβολές για να τις συσχετίσετε με το λογαριασμό σας. openid_desc=Το OpenID σας επιτρέπει να αναθέσετε τον έλεγχο ταυτότητας σε έναν εξωτερικό πάροχο. manage_ssh_keys=Διαχείριση SSH Κλειδιών @@ -721,7 +754,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" επαληθεύτηκε. @@ -769,7 +801,9 @@ ssh_disabled=SSH Απενεργοποιημένο ssh_signonly=Το SSH είναι απενεργοποιημένο αυτή τη στιγμή, έτσι αυτά τα κλειδιά είναι μόνο για την επαλήθευση υπογραφής των υποβολών. ssh_externally_managed=Αυτό το κλειδί SSH διαχειρίζεται εξωτερικά για αυτόν το χρήστη manage_social=Διαχείριση Συσχετιζόμενων Λογαριασμών Κοινωνικών Δικτύων +social_desc=Αυτοί οι κοινωνικοί λογαριασμοί μπορούν να χρησιμοποιηθούν για να συνδεθείτε στο λογαριασμό σας. Βεβαιωθείτε ότι τους αναγνωρίζετε όλους. unbind=Αποσύνδεση +unbind_success=Ο κοινωνικός λογαριασμός έχει διαγραφεί επιτυχώς. manage_access_token=Διαχείριση Διακριτικών Πρόσβασης generate_new_token=Δημιουργία Νέου Διακριτικού @@ -790,6 +824,8 @@ permissions_access_all=Όλα (δημόσια, ιδιωτικά, και περι select_permissions=Επιλέξτε δικαιώματα permission_no_access=Καμία Πρόσβαση permission_read=Αναγνωσμένες +permission_write=Ανάγνωση και Εγγραφή +access_token_desc=Τα επιλεγμένα δικαιώματα διακριτικών περιορίζουν την άδεια μόνο στις αντίστοιχες διαδρομές <a %s>API</a>. Διαβάστε την τεκμηρίωση <a %s></a> για περισσότερες πληροφορίες. at_least_one_permission=Πρέπει να επιλέξετε τουλάχιστον ένα δικαίωμα για να δημιουργήσετε ένα διακριτικό permissions_list=Δικαιώματα: @@ -801,6 +837,8 @@ remove_oauth2_application_desc=Η αφαίρεση μιας εφαρμογής O remove_oauth2_application_success=Η εφαρμογή έχει διαγραφεί. create_oauth2_application=Δημιουργία νέας εφαρμογής OAuth2 create_oauth2_application_button=Δημιουργία Εφαρμογής +create_oauth2_application_success=Έχετε δημιουργήσει με επιτυχία μια νέα εφαρμογή OAuth2. +update_oauth2_application_success=Έχετε ενημερώσει με επιτυχία την εφαρμογή OAuth2. oauth2_application_name=Όνομα Εφαρμογής oauth2_confidential_client=Εμπιστευτικός Πελάτης. Επιλέξτε το για εφαρμογές που διατηρούν το μυστικό κωδικό κρυφό, όπως πχ οι εφαρμογές ιστού. Μην επιλέγετε για εγγενείς εφαρμογές, συμπεριλαμβανομένων εφαρμογών επιφάνειας εργασίας και εφαρμογών για κινητά. oauth2_redirect_uris=URI Ανακατεύθυνσης. Χρησιμοποιήστε μια νέα γραμμή για κάθε URI. @@ -809,19 +847,26 @@ oauth2_client_id=Ταυτότητα Πελάτη oauth2_client_secret=Μυστικό Πελάτη oauth2_regenerate_secret=Αναδημιουργία Μυστικού oauth2_regenerate_secret_hint=Χάσατε το μυστικό σας; +oauth2_client_secret_hint=Το μυστικό δε θα εμφανιστεί ξανά αν κλείσετε ή ανανεώσετε αυτή τη σελίδα. Παρακαλώ βεβαιωθείτε ότι το έχετε αποθηκεύσει. oauth2_application_edit=Επεξεργασία oauth2_application_create_description=Οι εφαρμογές OAuth2 δίνει πρόσβαση στην εξωτερική εφαρμογή σας σε λογαριασμούς χρηστών σε αυτή την υπηρεσία. +oauth2_application_remove_description=Αφαιρώντας μια εφαρμογή OAuth2 θα αποτραπεί η πρόσβαση αυτής, σε εξουσιοδοτημένους λογαριασμούς χρηστών σε αυτή την υπηρεσία. Συνέχεια; +oauth2_application_locked=Το Gitea κάνει προεγγραφή σε μερικές εφαρμογές OAuth2 κατά την εκκίνηση αν είναι ενεργοποιημένες στις ρυθμίσεις. Για την αποφυγή απροσδόκητης συμπεριφοράς, αυτές δεν μπορούν ούτε να επεξεργαστούν ούτε να καταργηθούν. Παρακαλούμε ανατρέξτε στην τεκμηρίωση OAuth2 για περισσότερες πληροφορίες. authorized_oauth2_applications=Εξουσιοδοτημένες Εφαρμογές OAuth2 +authorized_oauth2_applications_description=Έχετε χορηγήσει πρόσβαση στον προσωπικό σας λογαριασμό σε αυτές τις εφαρμογές τρίτων. Ανακαλέστε την πρόσβαση για εφαρμογές που δεν χρειάζεστε πλέον. revoke_key=Ανάκληση revoke_oauth2_grant=Ανάκληση Πρόσβασης revoke_oauth2_grant_description=Η ανάκληση πρόσβασης για αυτή την εξωτερική εφαρμογή θα αποτρέψει αυτή την εφαρμογή από την πρόσβαση στα δεδομένα σας. Σίγουρα; +revoke_oauth2_grant_success=Η πρόσβαση ανακλήθηκε επιτυχώς. twofa_desc=Ο έλεγχος ταυτότητας δύο παραγόντων ενισχύει την ασφάλεια του λογαριασμού σας. +twofa_recovery_tip=Αν χάσετε τη συσκευή σας, θα είστε σε θέση να χρησιμοποιήσετε ένα κλειδί ανάκτησης μιας χρήσης για να ανακτήσετε την πρόσβαση στο λογαριασμό σας. twofa_is_enrolled=Ο λογαριασμός σας είναι <strong>εγγεγραμμένος</strong> σε έλεγχο ταυτότητας δύο παραγόντων. twofa_not_enrolled=Ο λογαριασμός σας δεν είναι εγγεγραμμένος σε έλεγχο ταυτότητας δύο παραγόντων. twofa_disable=Απενεργοποίηση Ταυτοποίησης Δύο Παραμέτρων twofa_scratch_token_regenerate=Αναδημιουργία Διακριτικού Μίας Χρήσης +twofa_scratch_token_regenerated=Το κλειδί ανάκτησης μιας χρήσης είναι τώρα %s. Αποθηκεύστε το σε ασφαλές μέρος, καθώς δε θα εμφανιστεί ξανά. twofa_enroll=Εγγραφή στην ταυτοποίηση δύο παραγόντων twofa_disable_note=Μπορείτε να απενεργοποιήσετε την ταυτοποίηση δύο παραγόντων αν χρειαστεί. twofa_disable_desc=Η απενεργοποίηση της ταυτοποίησης δύο παραγόντων θα καταστήσει τον λογαριασμό σας λιγότερο ασφαλή. Συνέχεια; @@ -839,6 +884,8 @@ webauthn_register_key=Προσθήκη Κλειδιού Ασφαλείας webauthn_nickname=Ψευδώνυμο webauthn_delete_key=Αφαίρεση Κλειδιού Ασφαλείας webauthn_delete_key_desc=Αν αφαιρέσετε ένα κλειδί ασφαλείας δεν μπορείτε πλέον να συνδεθείτε με αυτό. Συνέχεια; +webauthn_key_loss_warning=Αν χάσετε τα κλειδιά ασφαλείας σας, θα χάσετε την πρόσβαση στο λογαριασμό σας. +webauthn_alternative_tip=Μπορεί να θέλετε να ρυθμίσετε μια πρόσθετη μέθοδο ταυτοποίησης. manage_account_links=Διαχείριση Συνδεδεμένων Λογαριασμών manage_account_links_desc=Αυτοί οι εξωτερικοί λογαριασμοί είναι συνδεδεμένοι στον Gitea λογαριασμό σας. @@ -848,8 +895,10 @@ remove_account_link=Αφαίρεση Συνδεδεμένου Λογαριασμ remove_account_link_desc=Η κατάργηση ενός συνδεδεμένου λογαριασμού θα ανακαλέσει την πρόσβασή του στο λογαριασμό σας στο Gitea. Συνέχεια; remove_account_link_success=Ο συνδεδεμένος λογαριασμός έχει αφαιρεθεί. +hooks.desc=Προσθήκη webhooks που θα ενεργοποιούνται για <strong>όλα τα αποθετήρια</strong> που σας ανήκουν. orgs_none=Δεν είστε μέλος σε κάποιο οργανισμό. +repos_none=Δεν κατέχετε κάποιο αποθετήριο. delete_account=Διαγραφή Του Λογαριασμού Σας delete_prompt=Αυτή η ενέργεια θα διαγράψει μόνιμα το λογαριασμό σας. <strong>ΔΕΝ ΘΑ ΜΠΟΡΕΙ</strong> να επανέλθει. @@ -868,9 +917,12 @@ visibility=Ορατότητα χρήστη visibility.public=Δημόσια visibility.public_tooltip=Ορατό σε όλους visibility.limited=Περιορισμένη +visibility.limited_tooltip=Ορατό μόνο στους ταυτοποιημένους χρήστες visibility.private=Ιδιωτική +visibility.private_tooltip=Ορατό μόνο στα μέλη των οργανισμών που συμμετέχετε [repo] +new_repo_helper=Ένα αποθετήριο περιέχει όλα τα αρχεία έργου, συμπεριλαμβανομένου του ιστορικού εκδόσεων. Ήδη φιλοξενείται αλλού; <a href="%s">Μετεγκατάσταση αποθετηρίου.</a> owner=Ιδιοκτήτης owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων. repo_name=Όνομα αποθετηρίου @@ -882,6 +934,7 @@ template_helper=Μετατροπή σε πρότυπο αποθετήριο template_description=Τα πρότυπα αποθετήρια επιτρέπουν στους χρήστες να δημιουργήσουν νέα αποθετήρια με την ίδια δομή, αρχεία και προαιρετικές ρυθμίσεις. visibility=Ορατότητα visibility_description=Μόνο ο ιδιοκτήτης ή τα μέλη του οργανισμού εάν έχουν δικαιώματα, θα είναι σε θέση να το δουν. +visibility_helper=Αλλάξτε το αποθετήριο σε ιδιωτικό visibility_helper_forced=Ο διαχειριστής σας αναγκάζει τα νέα αποθετήρια να είναι ιδιωτικά. visibility_fork_helper=(Αλλάζοντας αυτό θα επηρεάσει όλα τα forks.) clone_helper=Χρειάζεστε βοήθεια για τη κλωνοποίηση; Επισκεφθείτε τη <a target="_blank" rel="noopener noreferrer" href="%s">Βοήθεια</a>. @@ -890,8 +943,10 @@ fork_from=Fork Από Το already_forked=Έχετε ήδη κάνει fork το %s fork_to_different_account=Fork σε διαφορετικό λογαριασμό fork_visibility_helper=Η ορατότητα ενός fork αποθετηρίου δεν μπορεί να αλλάξει. +fork_branch=Κλάδος που θα κλωνοποιηθεί στο fork +all_branches=Όλοι οι κλάδοι +fork_no_valid_owners=Αυτό το αποθετήριο δεν μπορεί να γίνει fork επειδή δεν υπάρχουν έγκυροι ιδιοκτήτες. use_template=Χρήση αυτού του πρότυπου -clone_in_vsc=Κλωνοποίηση στο VS Code download_zip=Λήψη ZIP download_tar=Λήψη TAR.GZ download_bundle=Κατεβάστε Το ΔΕΜΑ @@ -918,6 +973,7 @@ trust_model_helper_collaborator_committer=Συνεργάτης+Υποβολέα trust_model_helper_default=Προεπιλογή: Χρησιμοποιήστε το προεπιλεγμένο μοντέλο εμπιστοσύνης για αυτήν την εγκατάσταση create_repo=Δημιουργία Αποθετηρίου default_branch=Προεπιλεγμένος Κλάδος +default_branch_label=προεπιλογή default_branch_helper=Ο προεπιλεγμένος κλάδος είναι ο βασικός κλάδος για pull requests και υποβολές κώδικα. mirror_prune=Καθαρισμός mirror_prune_desc=Αφαίρεση παρωχημένων αναφορών απομακρυσμένης-παρακολούθησης @@ -926,6 +982,8 @@ mirror_interval_invalid=Το χρονικό διάστημα του ειδώλο mirror_sync_on_commit=Συγχρονισμός κατά την ώθηση mirror_address=Κλωνοποίηση Από Το URL mirror_address_desc=Τοποθετήστε όλα τα απαιτούμενα διαπιστευτήρια στην ενότητα Εξουσιοδότηση. +mirror_address_url_invalid=Η διεύθυνση URL που δόθηκε δεν είναι έγκυρη. Πρέπει να κάνετε escape όλα τα στοιχεία του url σωστά. +mirror_address_protocol_invalid=Η παρεχόμενη διεύθυνση URL δεν είναι έγκυρη. Μόνο οι τοποθεσίες http(s):// ή git:// μπορούν να χρησιμοποιηθούν για τη δημιουργία ειδώλου. mirror_lfs=Large File Storage (LFS) mirror_lfs_desc=Ενεργοποίηση αντικατοπτρισμού δεδομένων LFS. mirror_lfs_endpoint=Άκρο LFS @@ -951,13 +1009,20 @@ delete_preexisting=Διαγραφή αρχείων που προϋπήρχαν delete_preexisting_content=Διαγραφή αρχείων στο %s delete_preexisting_success=Διαγράφηκαν τα μη υιοθετημένα αρχεία στο %s blame_prior=Προβολή ευθύνης πριν από αυτή την αλλαγή +blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>. Πατήστε <a href="%s">εδώ</a> για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών. +blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>. author_search_tooltip=Εμφάνιση το πολύ 30 χρηστών +tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s +tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s +tree_path_not_found_tag=Η διαδρομή %[1]s δεν υπάρχει στην ετικέτα %[2]s transfer.accept=Αποδοχή Μεταφοράς transfer.accept_desc=`Μεταφορά στο "%s"` transfer.reject=Απόρριψη Μεταφοράς transfer.reject_desc=`Ακύρωση μεταφοράς σε "%s"` +transfer.no_permission_to_accept=Δεν έχετε άδεια να αποδεχτείτε αυτή τη μεταφορά. +transfer.no_permission_to_reject=Δεν έχετε άδεια να απορρίψετε αυτή τη μεταφορά. desc.private=Ιδιωτικό desc.public=Δημόσιο @@ -976,6 +1041,8 @@ template.issue_labels=Σήματα Ζητήματος template.one_item=Πρέπει να επιλέξετε τουλάχιστον ένα αντικείμενο στο πρότυπο template.invalid=Πρέπει να επιλέξετε ένα πρότυπο αποθετήριο +archive.title=Αυτό το αποθετήρειο αρχειοθετήθηκε. Μπορείτε να προβάλετε αρχεία και να τα κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests. +archive.title_date=Αυτό το αποθετήριο έχει αρχειοθετηθεί στο %s. Μπορείτε να προβάλετε αρχεία και να κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests. archive.issue.nocomment=Αυτό το αποθετήριο αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε σε ζητήματα. archive.pull.nocomment=Αυτό το repo αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε στα pull requests. @@ -992,6 +1059,7 @@ migrate_options_lfs=Μεταφορά αρχείων LFS migrate_options_lfs_endpoint.label=Άκρο LFS migrate_options_lfs_endpoint.description=Η μεταφορά θα προσπαθήσει να χρησιμοποιήσει το Git remote για να <a target="_blank" rel="noopener noreferrer" href="%s">καθορίσει τον διακομιστή LFS</a>. Μπορείτε επίσης να καθορίσετε ένα δικό σας endpoint αν τα δεδομένα LFS του αποθετηρίου αποθηκεύονται κάπου αλλού. migrate_options_lfs_endpoint.description.local=Μια διαδρομή στο τοπικό διακομιστή επίσης υποστηρίζεται. +migrate_options_lfs_endpoint.placeholder=Αν αφεθεί κενό, το άκρο θα προκύψει από το URL του κλώνου migrate_items=Στοιχεία Μεταφοράς migrate_items_wiki=Wiki migrate_items_milestones=Ορόσημα @@ -1094,6 +1162,10 @@ file_view_rendered=Προβολή Απόδοσης file_view_raw=Προβολή Ακατέργαστου file_permalink=Permalink file_too_large=Το αρχείο είναι πολύ μεγάλο για να εμφανιστεί. +invisible_runes_header=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode ` +invisible_runes_description=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode που δεν διακρίνονται από ανθρώπους, αλλά μπορεί να επεξεργάζονται διαφορετικά από έναν υπολογιστή. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.` +ambiguous_runes_header=`Αυτό το αρχείο περιέχει ασαφείς χαρακτήρες Unicode ` +ambiguous_runes_description=`Αυτό το αρχείο περιέχει χαρακτήρες Unicode που μπορεί να συγχέονται με άλλους χαρακτήρες. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.` invisible_runes_line=`Αυτή η γραμμή έχει αόρατους χαρακτήρες unicode ` ambiguous_runes_line=`Αυτή η γραμμή έχει ασαφείς χαρακτήρες unicode ` ambiguous_character=`ο %[1]c [U+%04[1]X] μπορεί να μπερδευτεί με τον %[2]c [U+%04[2]X]` @@ -1106,11 +1178,15 @@ video_not_supported_in_browser=Το πρόγραμμα περιήγησής σα audio_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 'audio'. stored_lfs=Αποθηκεύτηκε με το Git LFS symbolic_link=Symbolic link +executable_file=Εκτελέσιμο Αρχείο commit_graph=Γράφημα Υποβολών commit_graph.select=Επιλογή κλάδων commit_graph.hide_pr_refs=Απόκρυψη Pull Requests commit_graph.monochrome=Μονόχρωμο commit_graph.color=Έγχρωμο +commit.contained_in=Αυτή η υποβολή περιλαμβάνεται σε: +commit.contained_in_default_branch=Αυτή η υποβολή είναι μέρος του προεπιλεγμένου κλάδου +commit.load_referencing_branches_and_tags=Φόρτωση κλάδων και ετικετών που παραπέμπουν σε αυτήν την υποβολή blame=Ευθύνη download_file=Λήψη αρχείου normal_view=Κανονική Προβολή @@ -1189,9 +1265,7 @@ commits.desc=Δείτε το ιστορικό αλλαγών του πηγαίο commits.commits=Υποβολές commits.no_commits=Δεν υπάρχουν κοινές υποβολές. Τα "%s" και "%s" έχουν εντελώς διαφορετικές ιστορίες. commits.nothing_to_compare=Αυτοί οι κλάδοι είναι όμοιοι. -commits.search=Αναζήτηση υποβολών… commits.search.tooltip=Μπορείτε να προθέτετε τις λέξεις-κλειδιά με "author:", "committer:", "after:", ή "before:", π.χ. "επαναφορά author:Alice before:2019-01-13". -commits.find=Αναζήτηση commits.search_all=Όλοι Οι Κλάδοι commits.author=Συγγραφέας commits.message=Μήνυμα @@ -1203,6 +1277,7 @@ commits.signed_by_untrusted_user=Υπογράφηκε από μη έμπιστο commits.signed_by_untrusted_user_unmatched=Υπογράφηκε από ένα μη έμπιστο χρήστη ο οποίος δεν ταιριάζει με τον υποβολέα commits.gpg_key_id=ID Κλειδιού GPG commits.ssh_key_fingerprint=Αποτύπωμα Κλειδιού SSH +commits.view_path=Προβολή σε αυτή τη στιγμή στο ιστορικό commit.operations=Λειτουργίες commit.revert=Απόσυρση @@ -1241,7 +1316,6 @@ projects.type.basic_kanban=Βασικό 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=Όνομα @@ -1249,10 +1323,7 @@ projects.column.new_submit=Δημιουργία Στήλης projects.column.new=Νέα Στήλη projects.column.set_default=Ορισμός Προεπιλογής projects.column.set_default_desc=Ορίστε αυτή τη στήλη ως προεπιλογή για ζητήματα και pull requests χωρίς κατηγορία -projects.column.unset_default=Αφαίρεση Προεπιλογής -projects.column.unset_default_desc=Αφαίρεση της προεπιλογής αυτής της στήλης projects.column.delete=Διαγραφή Στήλης -projects.column.deletion_desc=Η διαγραφή μιας στήλης έργου μετακινεί όλα τα συναφή ζητήματα σε 'Χωρίς Κατηγορία'. Συνέχεια; projects.column.color=Έγχρωμο projects.open=Άνοιγμα projects.close=Κλείσιμο @@ -1330,6 +1401,7 @@ issues.delete_branch_at=`διέγραψε το κλάδο <b>%s</b> %s` issues.filter_label=Σήμα issues.filter_label_exclude=`Χρησιμοποιήστε <code>alt</code> + <code>κάντε κλικ/Enter</code> για να εξαιρέσετε τις σημάνσεις` issues.filter_label_no_select=Όλα τα σήματα +issues.filter_label_select_no_label=Χωρίς ετικέτα issues.filter_milestone=Ορόσημο issues.filter_milestone_all=Όλα τα ορόσημα issues.filter_milestone_none=Χωρίς ορόσημα @@ -1380,9 +1452,10 @@ issues.opened_by_fake=άνοιξε το %[1]s από %[2]s issues.closed_by_fake=από %[2]s έκλεισαν %[1]s issues.previous=Προηγούμενο issues.next=Επόμενο -issues.open_title=Ανοιχτά +issues.open_title=Ανοικτό issues.closed_title=Κλειστά issues.draft_title=Προσχέδιο +issues.num_comments_1=%d σχόλιο issues.num_comments=%d σχόλια issues.commented_at=`σχολίασε <a href="#%s">%s</a>` issues.delete_comment_confirm=Θέλετε σίγουρα να διαγράψετε αυτό το σχόλιο; @@ -1391,6 +1464,7 @@ issues.context.quote_reply=Παράθεση Απάντησης issues.context.reference_issue=Αναφορά σε νέο ζήτημα issues.context.edit=Επεξεργασία issues.context.delete=Διαγραφή +issues.no_content=Δεν υπάρχει περιγραφή. issues.close=Κλείσιμο Ζητήματος issues.comment_pull_merged_at=συγχώνευσε την υποβολή %[1]s στο %[2]s %[3]s issues.comment_manually_pull_merged_at=συγχώνευσε χειροκίνητα την υποβολή %[1]s στο %[2]s %[3]s @@ -1409,8 +1483,17 @@ issues.ref_closed_from=`<a href="%[3]s">έκλεισε αυτό το ζήτημ issues.ref_reopened_from=`<a href="%[3]s">άνοιξε ξανά αυτό το ζήτημα %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_from=`από %[1]s` issues.author=Συγγραφέας +issues.author_helper=Αυτός ο χρήστης είναι ο συγγραφέας. issues.role.owner=Ιδιοκτήτης +issues.role.owner_helper=Αυτός ο χρήστης είναι ο ιδιοκτήτης αυτού του αποθετηρίου. issues.role.member=Μέλος +issues.role.member_helper=Αυτός ο χρήστης είναι μέλος του οργανισμού που κατέχει αυτό το αποθετήριο. +issues.role.collaborator=Συνεργάτης +issues.role.collaborator_helper=Αυτός ο χρήστης έχει προσκληθεί να συνεργαστεί στο αποθετήριο. +issues.role.first_time_contributor=Συντελεστής για πρώτη φορά +issues.role.first_time_contributor_helper=Αυτή είναι η πρώτη συνεισφορά αυτού του χρήστη στο αποθετήριο. +issues.role.contributor=Συντελεστής +issues.role.contributor_helper=Αυτός ο χρήστης έχει προηγούμενές υποβολές στο αποθετήριο. issues.re_request_review=Επαναίτηση ανασκόπησης issues.is_stale=Έχουν υπάρξει αλλαγές σε αυτό το PR από αυτή την αναθεώρηση issues.remove_request_review=Αφαίρεση αιτήματος αναθεώρησης @@ -1425,6 +1508,9 @@ issues.label_title=Όνομα σήματος issues.label_description=Περιγραφή σήματος issues.label_color=Χρώμα σήματος issues.label_exclusive=Αποκλειστικό +issues.label_archive=Αρχειοθέτηση Σήματος +issues.label_archived_filter=Εμφάνιση αρχειοθετημένων σημάτων +issues.label_archive_tooltip=Τα αρχειοθετημένα σήματα εξαιρούνται από τις προτάσεις στην αναζήτηση με σήματα. issues.label_exclusive_desc=Ονομάστε το σήμα <code>πεδίο/στοιχείο</code> για να το κάνετε αμοιβαία αποκλειστικό με άλλα σήματα <code>πεδίου/</code>. issues.label_exclusive_warning=Τυχόν συγκρουόμενα σήματα θα αφαιρεθούν κατά την επεξεργασία των σημάτων ενός ζητήματος ή pull request. issues.label_count=%d σήματα @@ -1479,6 +1565,7 @@ issues.tracking_already_started=`Έχετε ήδη ξεκινήσει την κ issues.stop_tracking=Διακοπή Χρονομέτρου issues.stop_tracking_history=`σταμάτησε να εργάζεται %s` issues.cancel_tracking=Απόρριψη +issues.cancel_tracking_history=`ακύρωσε τη παρακολούθηση χρόνου %s` issues.add_time=Χειροκίνητη Προσθήκη Ώρας issues.del_time=Διαγραφή αυτού του αρχείου χρόνου issues.add_time_short=Προσθήκη Χρόνου @@ -1502,6 +1589,7 @@ issues.due_date_form=εεεε-μμ-ηη issues.due_date_form_add=Προσθήκη ημερομηνίας παράδοσης issues.due_date_form_edit=Επεξεργασία issues.due_date_form_remove=Διαγραφή +issues.due_date_not_writer=Χρειάζεστε πρόσβαση εγγραφής στο αποθετήριο για να ενημερώσετε την ημερομηνία λήξης ενός προβλήματος. issues.due_date_not_set=Δεν ορίστηκε ημερομηνία παράδοσης. issues.due_date_added=πρόσθεσε την ημερομηνία παράδοσης %s %s issues.due_date_modified=τροποποίησε την ημερομηνία παράδοσης από %[2]s σε %[1]s %[3]s @@ -1557,6 +1645,9 @@ issues.review.pending.tooltip=Αυτό το σχόλιο προς το παρό issues.review.review=Αξιολόγηση issues.review.reviewers=Εξεταστές issues.review.outdated=Παρωχημένο +issues.review.outdated_description=Το περιεχόμενο άλλαξε αφού έγινε αυτό το σχόλιο +issues.review.option.show_outdated_comments=Εμφάνιση παρωχημένων σχολίων +issues.review.option.hide_outdated_comments=Απόκρυψη παρωχημένων σχολίων issues.review.show_outdated=Εμφάνιση παροχημένων issues.review.hide_outdated=Απόκρυψη παροχημένων issues.review.show_resolved=Εμφάνιση επιλυμένων @@ -1595,7 +1686,13 @@ 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 +pulls.showing_specified_commit_range=Εμφάνιση μόνο των αλλαγών μεταξύ %[1]s..%[2]s +pulls.select_commit_hold_shift_for_range=Επιλέξτε υποβολή. Κρατήστε πατημένο το shift + κάντε κλικ για να επιλέξετε ένα εύρος +pulls.review_only_possible_for_full_diff=Η αξιολόγηση είναι δυνατή μόνο κατά την προβολή της πλήρης διαφοράς +pulls.filter_changes_by_commit=Φιλτράρισμα κατά υποβολή pulls.nothing_to_compare=Αυτοί οι κλάδοι είναι όμοιοι. Δεν υπάρχει ανάγκη να δημιουργήσετε ένα pull request. pulls.nothing_to_compare_and_allow_empty_pr=Αυτοί οι κλάδοι είναι ίσοι. Αυτό το PR θα είναι κενό. pulls.has_pull_request=`Υπάρχει ήδη pull request μεταξύ αυτών των κλάδων: <a href="%[1]s">%[2]s#%[3]d</a>` @@ -1627,6 +1724,12 @@ pulls.is_empty=Οι αλλαγές σε αυτόν τον κλάδο είναι pulls.required_status_check_failed=Ορισμένοι απαιτούμενοι έλεγχοι δεν ήταν επιτυχείς. pulls.required_status_check_missing=Λείπουν ορισμένοι απαιτούμενοι έλεγχοι. pulls.required_status_check_administrator=Ως διαχειριστής, μπορείτε ακόμα να συγχωνεύσετε αυτό το pull request. +pulls.blocked_by_approvals=Το pull request δεν έχει ακόμα αρκετές εγκρίσεις. Δόθηκαν %d από %d εγκρίσεις. +pulls.blocked_by_rejection=Αυτό το Pull Request έχει αλλαγές που ζητούνται από έναν επίσημο εξεταστή. +pulls.blocked_by_official_review_requests=Αυτό το Pull Request έχει επίσημες αιτήσεις αξιολόγησης. +pulls.blocked_by_outdated_branch=Αυτό το pull request έχει αποκλειστεί επειδή είναι παρωχημένο. +pulls.blocked_by_changed_protected_files_1=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει ένα προστατευμένο αρχείο: +pulls.blocked_by_changed_protected_files_n=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει προστατευμένα αρχεία: pulls.can_auto_merge_desc=Αυτό το Pull Request μπορεί να συγχωνευθεί αυτόματα. pulls.cannot_auto_merge_desc=Αυτό το pull request δεν μπορεί να συγχωνευθεί αυτόματα λόγω συγκρούσεων. pulls.cannot_auto_merge_helper=Χειροκίνητη Συγχώνευση για την επίλυση των συγκρούσεων. @@ -1661,6 +1764,7 @@ pulls.rebase_conflict_summary=Μήνυμα Σφάλματος pulls.unrelated_histories=H Συγχώνευση Απέτυχε: Η κεφαλή και η βάση της συγχώνευσης δεν μοιράζονται μια κοινή ιστορία. Συμβουλή: Δοκιμάστε μια διαφορετική στρατηγική pulls.merge_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, η βάση ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά. pulls.head_out_of_date=Η συγχώνευση απέτυχε: Κατά τη δημιουργία της συγχώνευσης, το HEAD ενημερώθηκε. Συμβουλή: Δοκιμάστε ξανά. +pulls.has_merged=Αποτυχία: Το pull request έχει συγχωνευθεί, δεν είναι δυνατή η συγχώνευση ξανά ή να αλλάξει ο κλάδος προορισμού. pulls.push_rejected=Η συγχώνευση απέτυχε: Η ώθηση απορρίφθηκε. Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο. pulls.push_rejected_summary=Μήνυμα Πλήρους Απόρριψης pulls.push_rejected_no_message=H Συγχώνευση Aπέτυχε: Η ώθηση απορρίφθηκε, αλλά δεν υπήρχε απομακρυσμένο μήνυμα.<br>Ελέγξτε τα Άγκιστρα Git για αυτό το αποθετήριο @@ -1672,6 +1776,8 @@ pulls.status_checks_failure=Κάποιοι έλεγχοι απέτυχαν pulls.status_checks_error=Ορισμένοι έλεγχοι ανέφεραν σφάλματα pulls.status_checks_requested=Απαιτείται pulls.status_checks_details=Λεπτομέρειες +pulls.status_checks_hide_all=Απόκρυψη όλων των ελέγχων +pulls.status_checks_show_all=Εμφάνιση όλων των ελέγχων pulls.update_branch=Ενημέρωση κλάδου με συγχώνευση pulls.update_branch_rebase=Ενημέρωση κλάδου με rebase pulls.update_branch_success=Η ενημέρωση του κλάδου ήταν επιτυχής @@ -1680,6 +1786,11 @@ pulls.outdated_with_base_branch=Αυτός ο κλάδος δεν είναι ε pulls.close=Κλείσιμο Pull Request pulls.closed_at=`έκλεισε αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at=`άνοιξε ξανά αυτό το pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.cmd_instruction_hint=`Δείτε τις <a class="show-instruction">οδηγίες γραμμής εντολών</a>.` +pulls.cmd_instruction_checkout_title=Έλεγχος +pulls.cmd_instruction_checkout_desc=Από το αποθετήριο του έργου σας, ελέγξτε έναν νέο κλάδο και δοκιμάστε τις αλλαγές. +pulls.cmd_instruction_merge_title=Συγχώνευση +pulls.cmd_instruction_merge_desc=Συγχώνευση των αλλαγών και ενημέρωση στο Gitea. pulls.clear_merge_message=Εκκαθάριση μηνύματος συγχώνευσης pulls.clear_merge_message_hint=Η εκκαθάριση του μηνύματος συγχώνευσης θα αφαιρέσει μόνο το περιεχόμενο του μηνύματος υποβολής και θα διατηρήσει τα παραγόμενα git trailers όπως "Co-Authored-By …". @@ -1698,7 +1809,9 @@ pulls.auto_merge_canceled_schedule_comment=`ακύρωσε την αυτόματ pulls.delete.title=Διαγραφή αυτού του pull request; pulls.delete.text=Θέλετε πραγματικά να διαγράψετε αυτό το pull request; (Αυτό θα σβήσει οριστικά όλο το περιεχόμενο του. Εξετάστε αν θέλετε να το κλείσετε, αν σκοπεύεται να το αρχειοθετήσετε) +pulls.recently_pushed_new_branches=Ωθήσατε στο κλάδο <strong>%[1]s</strong> %[2]s +pull.deleted_branch=(διαγράφηκε):%s milestones.new=Νέο Ορόσημο milestones.closed=Έκλεισε %s @@ -1706,6 +1819,7 @@ milestones.update_ago=Ενημερώθηκε %s milestones.no_due_date=Δεν υπάρχει ημερομηνία παράδοσης milestones.open=Άνοιγμα milestones.close=Κλείσιμο +milestones.new_subheader=Τα ορόσημα μπορούν να σας βοηθήσουν να οργανώσετε τα ζητήματα και να παρακολουθείτε την πρόοδό τους. milestones.completeness=%d%% Ολοκληρώθηκε milestones.create=Δημιουργία Ορόσημου milestones.title=Τίτλος @@ -1722,11 +1836,26 @@ milestones.edit_success=Το ορόσημο "%s" ενημερώθηκε. milestones.deletion=Διαγραφή Ορόσημου milestones.deletion_desc=Η διαγραφή ενός ορόσημου το αφαιρεί από όλα τα συναφή ζητήματα. Συνέχεια; milestones.deletion_success=Το ορόσημο έχει διαγραφεί. +milestones.filter_sort.earliest_due_data=Πλησιέστερη παράδοση +milestones.filter_sort.latest_due_date=Απώτερη παράδοση milestones.filter_sort.least_complete=Λιγότερο πλήρη milestones.filter_sort.most_complete=Περισσότερο πλήρη milestones.filter_sort.most_issues=Περισσότερα ζητήματα milestones.filter_sort.least_issues=Λιγότερα ζητήματα +signing.will_sign=Αυτή η υποβολή θα υπογραφεί με το κλειδί "%s". +signing.wont_sign.error=Παρουσιάστηκε σφάλμα κατά τον έλεγχο για το αν η υποβολή μπορεί να υπογραφεί. +signing.wont_sign.nokey=Δεν υπάρχει διαθέσιμο κλειδί για να υπογραφεί αυτή η υποβολή. +signing.wont_sign.never=Οι υποβολές δεν υπογράφονται ποτέ. +signing.wont_sign.always=Οι υποβολές υπογράφονται πάντα. +signing.wont_sign.pubkey=Η υποβολή δε θα υπογραφεί επειδή δεν υπάρχει δημόσιο κλειδί που να συνδέεται με το λογαριασμό σας. +signing.wont_sign.twofa=Πρέπει να έχετε ενεργοποιημένη την ταυτοποίηση δύο παραγόντων για να υπογράφεται υποβολές. +signing.wont_sign.parentsigned=Η υποβολή δε θα υπογραφεί καθώς η γονική υποβολή δεν έχει υπογραφεί. +signing.wont_sign.basesigned=Η συγχώνευση δε θα υπογραφεί καθώς η βασική υποβολή δεν έχει υπογραφή της βάσης. +signing.wont_sign.headsigned=Η συγχώνευση δε θα υπογραφεί καθώς δεν έχει υπογραφή η υποβολή της κεφαλής. +signing.wont_sign.commitssigned=Η συγχώνευση δε θα υπογραφεί καθώς όλες οι σχετικές υποβολές δεν έχουν υπογραφεί. +signing.wont_sign.approved=Η συγχώνευση δε θα υπογραφεί καθώς το PR δεν έχει εγκριθεί. +signing.wont_sign.not_signed_in=Δεν είστε συνδεδεμένοι. ext_wiki=Πρόσβαση στο Εξωτερικό Wiki ext_wiki.desc=Σύνδεση σε ένα εξωτερικό wiki. @@ -1824,16 +1953,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=Fuzzy -search.fuzzy.tooltip=Συμπερίληψη και των αποτελεσμάτων που είναι πλησιέστερα με τον όρο αναζήτησης -search.match=Ταίριασμα -search.match.tooltip=Συμπερίληψη μόνο των αποτελεσμάτων που ταιριάζουν ακριβώς με τον όρο αναζήτησης -search.results=Αποτελέσματα αναζήτησης για "%s" σε <a href="%s">%s</a> -search.code_no_results=Δεν βρέθηκε πηγαίος κώδικας που να ταιριάζει με τον όρο αναζήτησης. -search.code_search_unavailable=Η αναζήτηση κώδικα δεν είναι διαθέσιμη αυτή τη στιγμή. Παρακαλώ επικοινωνήστε με το διαχειριστή. +contributors.contribution_type.commits=Υποβολές settings=Ρυθμίσεις settings.desc=Στις Ρυθμίσεις μπορείτε να διαχειριστείτε τις ρυθμίσεις για το αποθετήριο @@ -1856,7 +1976,9 @@ settings.mirror_settings.docs.disabled_push_mirror.info=Τα είδωλα ώθη settings.mirror_settings.docs.no_new_mirrors=Το αποθετήριο σας αντιγράφει τις αλλαγές προς ή από ένα άλλο αποθετήριο. Λάβετε υπόψη ότι δεν μπορείτε να δημιουργήσετε νέα είδωλα αυτή τη στιγμή. settings.mirror_settings.docs.can_still_use=Αν και δεν μπορείτε να τροποποιήσετε τα υπάρχοντα είδωλα ή να δημιουργήσετε νέα, μπορείτε να χρησιμοποιείται ακόμα το υπάρχων είδωλο. settings.mirror_settings.docs.pull_mirror_instructions=Για να ορίσετε έναν είδωλο έλξης, παρακαλούμε συμβουλευθείτε: +settings.mirror_settings.docs.more_information_if_disabled=Μπορείτε να μάθετε περισσότερα για τα είδωλα ώθησης και έλξης εδώ: settings.mirror_settings.docs.doc_link_title=Πώς μπορώ να αντιγράψω αποθετήρια; +settings.mirror_settings.docs.doc_link_pull_section=το κεφάλαιο "Pulling from a remote repository" της τεκμηρίωσης. settings.mirror_settings.docs.pulling_remote_title=Έλξη από ένα απομακρυσμένο αποθετήριο settings.mirror_settings.mirrored_repository=Είδωλο αποθετηρίου settings.mirror_settings.direction=Κατεύθυνση @@ -1866,8 +1988,11 @@ settings.mirror_settings.last_update=Τελευταία ενημέρωση settings.mirror_settings.push_mirror.none=Δεν έχουν ρυθμιστεί είδωλα ώθησης settings.mirror_settings.push_mirror.remote_url=URL Απομακρυσμένου Αποθετηρίου Git settings.mirror_settings.push_mirror.add=Προσθήκη Είδωλου Push +settings.mirror_settings.push_mirror.edit_sync_time=Επεξεργασία διαστήματος συγχρονισμού ειδώλου settings.sync_mirror=Συγχρονισμός Τώρα +settings.pull_mirror_sync_in_progress=Έλκονται αλλαγές από το απομακρυσμένο %s αυτή τη στιγμή. +settings.push_mirror_sync_in_progress=Ώθηση αλλαγών στο απομακρυσμένο %s αυτή τη στιγμή. settings.site=Ιστοσελίδα settings.update_settings=Ενημέρωση Ρυθμίσεων settings.update_mirror_settings=Ενημέρωση Ρυθμίσεων Ειδώλου @@ -1907,6 +2032,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) @@ -1934,6 +2060,7 @@ settings.transfer.rejected=Η μεταβίβαση του αποθετηρίου settings.transfer.success=Η μεταβίβαση του αποθετηρίου ήταν επιτυχής. settings.transfer_abort=Ακύρωση μεταβίβασης settings.transfer_abort_invalid=Δεν μπορείτε να ακυρώσετε μια ανύπαρκτη μεταβίβαση αποθετηρίου. +settings.transfer_abort_success=Η μεταφορά αποθετηρίου στο %s ακυρώθηκε με επιτυχία. settings.transfer_desc=Μεταβιβάστε αυτό το αποθετήριο σε έναν χρήστη ή σε έναν οργανισμό για τον οποίο έχετε δικαιώματα διαχειριστή. settings.transfer_form_title=Εισάγετε το όνομα του αποθετηρίου ως επιβεβαίωση: settings.transfer_in_progress=Αυτή τη στιγμή υπάρχει μια εν εξελίξει μεταβίβαση. Παρακαλούμε ακυρώστε την αν θέλετε να μεταβιβάσετε αυτό το αποθετήριο σε άλλο χρήστη. @@ -1980,7 +2107,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=Η ομάδα δεν είναι στον ίδιο οργανισμό με το αποθετήριο @@ -1988,7 +2114,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=Έχει αφαιρεθεί η πρόσβαση της ομάδας στο αποθετήριο. @@ -2000,12 +2125,14 @@ settings.webhook_deletion_desc=Η αφαίρεση ενός webhook διαγρά settings.webhook_deletion_success=Το webhook έχει αφαιρεθεί. settings.webhook.test_delivery=Δοκιμή Παράδοσης settings.webhook.test_delivery_desc=Δοκιμάστε αυτό το webhook με ένα ψεύτικο συμβάν. +settings.webhook.test_delivery_desc_disabled=Για να δοκιμάσετε αυτό το webhook με μια ψεύτικη κλήση, ενεργοποιήστε το. settings.webhook.request=Αίτημα settings.webhook.response=Απάντηση settings.webhook.headers=Κεφαλίδες settings.webhook.payload=Περιεχόμενο settings.webhook.body=Σώμα settings.webhook.replay.description=Επανάληψη αυτού του webhook. +settings.webhook.replay.description_disabled=Για να επαναλάβετε αυτό το webhook, ενεργοποιήστε το. settings.webhook.delivery.success=Ένα γεγονός έχει προστεθεί στην ουρά παράδοσης. Μπορεί να χρειαστούν λίγα δευτερόλεπτα μέχρι να εμφανιστεί στο ιστορικό. settings.githooks_desc=Τα Άγκιστρα Git παρέχονται από το ίδιο το Git. Μπορείτε να επεξεργαστείτε τα αρχεία αγκίστρων παρακάτω για να ρυθμίσετε προσαρμοσμένες λειτουργίες. settings.githook_edit_desc=Αν το hook είναι ανενεργό, θα παρουσιαστεί ένα παράδειγμα. Αφήνοντας το περιεχόμενο του hook κενό θα το απενεργοποιήσετε. @@ -2139,9 +2266,7 @@ settings.protect_whitelist_committers=Περιορισμός του Push στη 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=Ενεργοποίηση Λίστας Συγχώνευσης settings.protect_merge_whitelist_committers_desc=Επιτρέψτε μόνο σε χρήστες ή ομάδες στη λίστα να συγχωνεύσουν pull requests σε αυτό το κλάδο. settings.protect_merge_whitelist_users=Λίστα επιτρεπόμενων χρηστών για συγχώνευση: @@ -2165,6 +2290,7 @@ settings.dismiss_stale_approvals_desc=Όταν οι νέες υποβολές π settings.require_signed_commits=Απαιτούνται Υπογεγραμμένες Υποβολές settings.require_signed_commits_desc=Απόρριψη νέων υποβολών σε αυτόν τον κλάδο εάν είναι μη υπογεγραμμένες ή μη επαληθεύσιμες. settings.protect_branch_name_pattern=Μοτίβο Προστατευμένου Ονόματος Κλάδου +settings.protect_branch_name_pattern_desc=Μοτίβα ονόματος προστατευμένων κλάδων. Δείτε <a href="https://github.com/gobwas/glob">την τεκμηρίωση</a> για σύνταξη μοτίβου. Παραδείγματα: main, release/** settings.protect_patterns=Μοτίβα settings.protect_protected_file_patterns=Μοτίβα προστατευμένων αρχείων (διαχωρισμένα με ερωτηματικό ';'): settings.protect_protected_file_patterns_desc=Τα προστατευόμενα αρχεία δεν επιτρέπεται να αλλάξουν άμεσα, ακόμη και αν ο χρήστης έχει δικαιώματα να προσθέσει, να επεξεργαστεί ή να διαγράψει αρχεία σε αυτόν τον κλάδο. Επιπλέων μοτίβα μπορούν να διαχωριστούν με ερωτηματικό (';'). Δείτε την τεκμηρίωση <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> για τη σύνταξη του μοτίβου. Πχ: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. @@ -2201,18 +2327,26 @@ settings.tags.protection.allowed.teams=Επιτρεπόμενες ομάδες settings.tags.protection.allowed.noone=Καμία settings.tags.protection.create=Προστασία Ετικέτας settings.tags.protection.none=Δεν υπάρχουν προστατευμένες ετικέτες. +settings.tags.protection.pattern.description=Μπορείτε να χρησιμοποιήσετε ένα μόνο όνομα ή ένα μοτίβο τύπου glob ή κανονική έκφραση για να ταιριάξετε πολλαπλές ετικέτες. Διαβάστε περισσότερα στον <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">οδηγό προστατευμένων ετικετών</a>. settings.bot_token=Διακριτικό Bot settings.chat_id=ID Συνομιλίας +settings.thread_id=ID Νήματος settings.matrix.homeserver_url=Homeserver URL settings.matrix.room_id=ID Δωματίου settings.matrix.message_type=Τύπος Μηνύματος settings.archive.button=Αρχειοθέτηση Αποθετηρίου settings.archive.header=Αρχειοθέτηση Αυτού του Αποθετηρίου +settings.archive.text=Η αρχειοθέτηση του αποθετηρίου θα το αλλάξει σε μόνο για ανάγνωση. Δε θα φαίνεται στον αρχικό πίνακα. Κανείς (ακόμα και εσείς!) δε θα μπορεί να κάνει νέες υποβολές, ή να ανοίξει ζητήματα ή pull request. settings.archive.success=Το αποθετήριο αρχειοθετήθηκε με επιτυχία. settings.archive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια αρχειοθέτησης του αποθετηρίου. Δείτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες. settings.archive.error_ismirror=Δε μπορείτε να αρχειοθετήσετε ένα είδωλο αποθετηρίου. settings.archive.branchsettings_unavailable=Οι ρυθμίσεις του κλάδου δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο. settings.archive.tagsettings_unavailable=Οι ρυθμίσεις της ετικέτας δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο. +settings.unarchive.button=Απο-Αρχειοθέτηση αποθετηρίου +settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου +settings.unarchive.text=Η απο-αρχειοθέτηση του αποθετηρίου θα αποκαταστήσει την ικανότητά του να λαμβάνει υποβολές και ωθήσεις, καθώς και νέα ζητήματα και pull-requests. +settings.unarchive.success=Το αποθετήριο απο-αρχειοθετήθηκε με επιτυχία. +settings.unarchive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια απο-αρχειοθέτησης του αποθετηρίου. Δείτε τις καταγραφές για περισσότερες λεπτομέρειες. settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί. settings.lfs=LFS settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο @@ -2279,6 +2413,7 @@ diff.show_more=Εμφάνιση Περισσότερων diff.load=Φόρτωση Διαφορών diff.generated=δημιουργημένο diff.vendored=εξωτερικό +diff.comment.add_line_comment=Προσθήκη σχολίου στη γραμμή diff.comment.placeholder=Αφήστε ένα σχόλιο diff.comment.markdown_info=Υποστηρίζεται στυλ με markdown. diff.comment.add_single_comment=Προσθέστε ένα σχόλιο @@ -2335,6 +2470,7 @@ release.edit_release=Ενημέρωση Κυκλοφορίας release.delete_release=Διαγραφή Κυκλοφορίας release.delete_tag=Διαγραφή Ετικέτας release.deletion=Διαγραφή Κυκλοφορίας +release.deletion_desc=Διαγράφοντας μια κυκλοφορία, αυτή αφαιρείται μόνο από το Gitea. Δε θα επηρεάσει την ετικέτα Git, τα περιεχόμενα του αποθετηρίου σας ή το ιστορικό της. Συνέχεια; release.deletion_success=Η κυκλοφορία έχει διαγραφεί. release.deletion_tag_desc=Θα διαγράψει αυτή την ετικέτα από το αποθετήριο. Τα περιεχόμενα του αποθετηρίου και το ιστορικό παραμένουν αμετάβλητα. Συνέχεια; release.deletion_tag_success=Η ετικέτα έχει διαγραφεί. @@ -2354,6 +2490,7 @@ branch.already_exists=Ήδη υπάρχει ένας κλάδος με το όν branch.delete_head=Διαγραφή branch.delete=`Διαγραφή του Κλάδου "%s"` branch.delete_html=Διαγραφή Κλάδου +branch.delete_desc=Η διαγραφή ενός κλάδου είναι μόνιμη. Αν και ο διαγραμμένος κλάδος μπορεί να συνεχίσει να υπάρχει για σύντομο χρονικό διάστημα πριν να αφαιρεθεί, ΔΕΝ ΜΠΟΡΕΙ να αναιρεθεί στις περισσότερες περιπτώσεις. Συνέχεια; branch.deletion_success=Ο κλάδος "%s" διαγράφηκε. branch.deletion_failed=Αποτυχία διαγραφής του κλάδου "%s". branch.delete_branch_has_new_commits=Ο κλάδος "%s" δεν μπορεί να διαγραφεί επειδή προστέθηκαν νέες υποβολές μετά τη συγχώνευση. @@ -2393,6 +2530,7 @@ tag.create_success=Η ετικέτα "%s" δημιουργήθηκε. topic.manage_topics=Διαχείριση Θεμάτων topic.done=Ολοκληρώθηκε topic.count_prompt=Δεν μπορείτε να επιλέξετε περισσότερα από 25 θέματα +topic.format_prompt=Τα θέματα πρέπει να ξεκινούν με γράμμα ή αριθμό, μπορούν να περιλαμβάνουν παύλες ('-') και τελείες ('.'), μπορεί να είναι μέχρι 35 χαρακτήρες. Τα γράμματα πρέπει να είναι πεζά. find_file.go_to_file=Αναζήτηση αρχείου find_file.no_matching=Δεν ταιριάζει κανένα αρχείο @@ -2401,6 +2539,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=Πλήρες Όνομα Οργανισμού @@ -2431,6 +2571,7 @@ form.create_org_not_allowed=Δεν επιτρέπεται να δημιουργ settings=Ρυθμίσεις settings.options=Οργανισμός settings.full_name=Πλήρες Όνομα +settings.email=Email Επικοινωνίας settings.website=Ιστοσελίδα settings.location=Τοποθεσία settings.permission=Δικαιώματα @@ -2444,6 +2585,7 @@ settings.visibility.private_shortname=Ιδιωτικός settings.update_settings=Ενημέρωση Ρυθμίσεων settings.update_setting_success=Οι ρυθμίσεις του οργανισμού έχουν ενημερωθεί. +settings.change_orgname_prompt=Σημείωση: Η αλλαγή του ονόματος του οργανισμού θα αλλάξει επίσης τη διεύθυνση URL του οργανισμού σας και θα απελευθερώσει το παλιό όνομα. settings.change_orgname_redirect_prompt=Το παλιό όνομα θα ανακατευθύνει μέχρι να διεκδικηθεί. settings.update_avatar_success=Η εικόνα του οργανισμού έχει ενημερωθεί. settings.delete=Διαγραφή Οργανισμού @@ -2503,7 +2645,6 @@ teams.write_permission_desc=Αυτή η ομάδα χορηγεί πρόσβασ teams.admin_permission_desc=Αυτή η ομάδα παρέχει πρόσβαση <strong>Διαχειριστή</strong>: τα μέλη μπορούν να διαβάσουν, να κάνουν push και να προσθέσουν συνεργάτες στα αποθετήρια της ομάδας. teams.create_repo_permission_desc=Επιπλέον, αυτή η ομάδα χορηγεί άδεια <strong>Δημιουργία αποθετηρίου</strong>: τα μέλη μπορούν να δημιουργήσουν νέα αποθετήρια στον οργανισμό. teams.repositories=Αποθετήρια Ομάδας -teams.search_repo_placeholder=Αναζήτηση αποθετηρίου… teams.remove_all_repos_title=Αφαίρεση όλων των αποθετηρίων της ομάδας teams.remove_all_repos_desc=Αυτό θα αφαιρέσει όλα τα αποθετήρια από την ομάδα. teams.add_all_repos_title=Προσθήκη όλων των αποθετηρίων @@ -2519,27 +2660,33 @@ teams.all_repositories_helper=Η ομάδα έχει πρόσβαση σε όλ teams.all_repositories_read_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση <strong>Ανάγνωσης</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να δουν και να κλωνοποιήσουν αποθετήρια. teams.all_repositories_write_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση <strong>Εγγραφής</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να διαβάσουν και να κάνουν push σε αποθετήρια. teams.all_repositories_admin_permission_desc=Αυτή η ομάδα παρέχει πρόσβαση <strong>Διαχείρισης</strong> σε <strong>όλα τα αποθετήρια</strong>: τα μέλη μπορούν να διαβάσουν, να κάνουν push και να προσθέσουν συνεργάτες στα αποθετήρια. +teams.invite.title=Έχετε προσκληθεί να συμμετάσχετε στην ομάδα <strong>%s</strong> του οργανισμού <strong>%s</strong>. teams.invite.by=Προσκλήθηκε από %s teams.invite.description=Παρακαλώ κάντε κλικ στον παρακάτω σύνδεσμο για συμμετοχή στην ομάδα. [admin] dashboard=Πίνακας Ελέγχου +identity_access=Ταυτότητα & Πρόσβαση users=Λογαριασμοί Χρήστη organizations=Οργανισμοί +assets=Στοιχεία Κώδικα repositories=Αποθετήρια hooks=Webhooks +integrations=Ενσωματώσεις authentication=Πηγές Ταυτοποίησης emails=Email Χρήστη config=Διαμόρφωση +config_summary=Περίληψη +config_settings=Ρυθμίσεις notices=Ειδοποιήσεις Συστήματος monitor=Παρακολούθηση first_page=Πρώτο last_page=Τελευταίο total=Σύνολο: %d +settings=Ρυθμίσεις Διαχειριστή dashboard.new_version_hint=Το Gitea %s είναι διαθέσιμο, τώρα εκτελείτε το %s. Ανατρέξτε <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">στο blog</a> για περισσότερες λεπτομέρειες. dashboard.statistic=Περίληψη -dashboard.operations=Λειτουργίες Συντήρησης dashboard.system_status=Κατάσταση Συστήματος dashboard.operation_name=Όνομα Λειτουργίας dashboard.operation_switch=Αλλαγή @@ -2548,11 +2695,13 @@ dashboard.clean_unbind_oauth=Εκκαθάριση μη δεσμευμένων σ dashboard.clean_unbind_oauth_success=Όλες οι μη δεσμευμένες συνδέσεις OAuth διαγράφηκαν. dashboard.task.started=Εκκίνηση Εργασίας: %[1]s dashboard.task.process=Εργασία: %[1]s +dashboard.task.cancelled=Εργασία: %[1]ακυρώθηκε: %[3]s dashboard.task.error=Σφάλμα στην Εργασία: %[1]s: %[3]s dashboard.task.finished=Εργασία: %[1]s που εκκινήθηκε από %[2]s τελείωσε dashboard.task.unknown=Άγνωστη εργασία: %[1]s dashboard.cron.started=Εκκίνηση Προγραμματισμένης Εργασίας: %[1]s dashboard.cron.process=Προγραμματισμένη Εργασία: %[1]s +dashboard.cron.cancelled=Προγραμματισμένη εργασία: %[1]s ακυρώθηκε: %[3]s dashboard.cron.error=Σφάλμα στη Προγραμματισμένη Εργασία: %s: %[3]s dashboard.cron.finished=Προγραμματισμένη Εργασία: %[1]s τελείωσε dashboard.delete_inactive_accounts=Διαγραφή όλων των μη ενεργοποιημένων λογαριασμών @@ -2562,6 +2711,7 @@ dashboard.delete_repo_archives.started=Η διαγραφή όλων των αρ dashboard.delete_missing_repos=Διαγραφή όλων των αποθετηρίων που δεν έχουν τα αρχεία Git τους dashboard.delete_missing_repos.started=Η διαγραφή όλων των αποθετηρίων που δεν έχουν αρχεία Git τους, ξεκίνησε. dashboard.delete_generated_repository_avatars=Διαγραφή δημιουργημένων εικόνων αποθετηρίων +dashboard.sync_repo_branches=Συγχρονισμός κλάδων που λείπουν, από τα δεδομένα git στις βάσεις δεδομένων dashboard.update_mirrors=Ενημέρωση Ειδώλων dashboard.repo_health_check=Έλεγχος υγείας σε όλα τα αποθετήρια dashboard.check_repo_stats=Έλεγχος όλων των στατιστικών αποθετηρίων @@ -2576,6 +2726,7 @@ dashboard.reinit_missing_repos=Επανεκκινήστε όλα τα αποθε dashboard.sync_external_users=Συγχρονισμός δεδομένων εξωτερικών χρηστών dashboard.cleanup_hook_task_table=Εκκαθάριση πίνακα hook_task dashboard.cleanup_packages=Εκκαθάριση ληγμένων πακέτων +dashboard.cleanup_actions=Οι ενέργειες καθαρισμού καταγραφές και αντικείμενα dashboard.server_uptime=Διάρκεια Διακομιστή dashboard.current_goroutine=Τρέχουσες Goroutines dashboard.current_memory_usage=Τρέχουσα Χρήση Μνήμης @@ -2613,6 +2764,9 @@ dashboard.gc_lfs=Συλλογή απορριμάτων στα μετα-αντι dashboard.stop_zombie_tasks=Διακοπή εργασιών ζόμπι dashboard.stop_endless_tasks=Διακοπή ατελείωτων εργασιών dashboard.cancel_abandoned_jobs=Ακύρωση εγκαταλελειμμένων εργασιών +dashboard.start_schedule_tasks=Έναρξη προγραμματισμένων εργασιών +dashboard.sync_branch.started=Ο Συγχρονισμός των Κλάδων ξεκίνησε +dashboard.rebuild_issue_indexer=Αναδόμηση ευρετηρίου ζητημάτων users.user_manage_panel=Διαχείριση Λογαριασμών Χρηστών users.new_account=Δημιουργία Λογαριασμού Χρήστη @@ -2621,6 +2775,9 @@ users.full_name=Πλήρες Όνομα users.activated=Ενεργοποιήθηκε users.admin=Διαχειριστής users.restricted=Περιορισμένος +users.reserved=Δεσμευμένο +users.bot=Bot +users.remote=Απομακρυσμένο users.2fa=2FA users.repos=Αποθετήρια users.created=Δημιουργήθηκε @@ -2667,6 +2824,7 @@ users.list_status_filter.is_prohibit_login=Απαγόρευση Σύνδεσης users.list_status_filter.not_prohibit_login=Επιτρέπεται η Σύνδεση users.list_status_filter.is_2fa_enabled=2FA Ενεργοποιημένο users.list_status_filter.not_2fa_enabled=2FA Απενεργοποιημένο +users.details=Λεπτομέρειες Χρήστη emails.email_manage_panel=Διαχείριση Email Χρήστη emails.primary=Κύριο @@ -2679,6 +2837,7 @@ emails.updated=Το email ενημερώθηκε emails.not_updated=Αποτυχία ενημέρωσης της ζητούμενης διεύθυνσης email: %v emails.duplicate_active=Αυτή η διεύθυνση email είναι ήδη ενεργή σε διαφορετικό χρήστη. emails.change_email_header=Ενημέρωση Ιδιοτήτων Email +emails.change_email_text=Είστε βέβαιοι ότι θέλετε να ενημερώσετε αυτή τη διεύθυνση email; orgs.org_manage_panel=Διαχείριση Οργανισμού orgs.name=Όνομα @@ -2692,15 +2851,15 @@ repos.unadopted.no_more=Δεν βρέθηκαν μη υιοθετημένα απ repos.owner=Ιδιοκτήτης repos.name=Όνομα repos.private=Ιδιωτικό -repos.watches=Παρακολουθήσεις -repos.stars=Αστέρια -repos.forks=Forks repos.issues=Ζητήματα repos.size=Μέγεθος +repos.lfs_size=Μέγεθος LFS packages.package_manage_panel=Διαχείριση Πακέτων packages.total_size=Συνολικό Μέγεθος: %s packages.unreferenced_size=Μέγεθος Χωρίς Αναφορά: %s +packages.cleanup=Εκκαθάριση ληγμένων δεδομένων +packages.cleanup.success=Επιτυχής εκκαθάριση δεδομένων που έχουν λήξει packages.owner=Ιδιοκτήτης packages.creator=Δημιουργός packages.name=Όνομα @@ -2711,10 +2870,12 @@ packages.size=Μέγεθος packages.published=Δημοσιευμένα defaulthooks=Προεπιλεγμένα Webhooks +defaulthooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούν ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ είναι προκαθορισμένα και θα αντιγραφούν σε όλα τα νέα αποθετήρια. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>. defaulthooks.add_webhook=Προσθήκη Προεπιλεγμένου Webhook defaulthooks.update_webhook=Ενημέρωση Προεπιλεγμένου Webhook systemhooks=Webhooks Συστήματος +systemhooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούνται ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ θα ενεργούν σε όλα τα αποθετήρια του συστήματος, γι 'αυτό παρακαλώ εξετάστε τυχόν επιπτώσεις απόδοσης που μπορεί να έχει. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>. systemhooks.add_webhook=Προσθήκη Webhook Συστήματος systemhooks.update_webhook=Ενημέρωση Webhook Συστήματος @@ -2807,17 +2968,18 @@ auths.sspi_default_language=Προεπιλεγμένη γλώσσα χρήστη auths.sspi_default_language_helper=Προεπιλεγμένη γλώσσα για τους χρήστες που δημιουργούνται αυτόματα με τη μέθοδο ταυτοποίησης SSPI. Αφήστε κενό αν προτιμάτε η γλώσσα να εντοπιστεί αυτόματα. auths.tips=Συμβουλές auths.tips.oauth2.general=Ταυτοποίηση OAuth2 +auths.tips.oauth2.general.tip=Κατά την εγγραφή μιας νέας ταυτοποίησης OAuth2, το URL κλήσης/ανακατεύθυνσης πρέπει να είναι: auths.tip.oauth2_provider=Πάροχος OAuth2 auths.tip.bitbucket=Καταχωρήστε ένα νέο καταναλωτή OAuth στο https://bitbucket.org/account/user/<your username>/oauth-consumers/new και προσθέστε το δικαίωμα 'Account' - 'Read' auths.tip.nextcloud=`Καταχωρήστε ένα νέο καταναλωτή OAuth στην υπηρεσία σας χρησιμοποιώντας το παρακάτω μενού "Settings -> Security -> OAuth 2.0 client"` 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 (<server>/.well known/openid-configuration) για να καθορίσετε τα τελικά σημεία auths.tip.twitter=Πηγαίνετε στο https://dev.twitter.com/apps, δημιουργήστε μια εφαρμογή και βεβαιωθείτε ότι η επιλογή “Allow this application to be used to Sign in with Twitter” είναι ενεργοποιημένη auths.tip.discord=Καταχωρήστε μια νέα εφαρμογή στο https://discordapp.com/developers/applications/me +auths.tip.gitea=Καταχωρήστε μια νέα εφαρμογή OAuth2. Μπορείτε να βρείτε τον οδηγό στο https://docs.gitea.com/development/oauth2-provider auths.tip.yandex=`Δημιουργήστε μια νέα εφαρμογή στο https://oauth.yandex.com/client/new. Επιλέξτε τα ακόλουθα δικαιώματα από την ενότητα "Yandex.Passport API": "Access to email address", "Access to user avatar" και "Access to username, first name and surname, gender"` auths.tip.mastodon=Εισαγάγετε ένα προσαρμομένο URL για την υπηρεσία mastodon με την οποία θέλετε να πιστοποιήσετε (ή να χρησιμοποιήσετε την προεπιλεγμένη) auths.edit=Επεξεργασία Πηγής Ταυτοποίησης @@ -2847,6 +3009,7 @@ config.disable_router_log=Απενεργοποίηση Καταγραφής Δρ config.run_user=Εκτέλεση Σαν Χρήστη config.run_mode=Λειτουργία Εκτέλεσης config.git_version=Έκδοση Git +config.app_data_path=Διαδρομή Δεδομένων Εφαρμογής config.repo_root_path=Ριζική Διαδρομή Αποθετηρίων config.lfs_root_path=Ριζική Διαδρομή LFS config.log_file_root_path=Διαδρομή Καταγραφών @@ -2996,8 +3159,10 @@ monitor.queue.name=Όνομα monitor.queue.type=Τύπος monitor.queue.exemplar=Τύπος Υποδείγματος monitor.queue.numberworkers=Αριθμός Εργατών +monitor.queue.activeworkers=Ενεργοί Εργάτες monitor.queue.maxnumberworkers=Μέγιστος Αριθμός Εργατών monitor.queue.numberinqueue=Πλήθος Ουράς +monitor.queue.review_add=Εξέταση / Προσθήκη Εργατών monitor.queue.settings.title=Ρυθμίσεις Δεξαμενής monitor.queue.settings.desc=Οι δεξαμενές αυξάνονται δυναμικά όταν υπάρχει φραγή της ουράς των εργατών τους. monitor.queue.settings.maxnumberworkers=Μέγιστος Αριθμός Εργατών @@ -3023,6 +3188,7 @@ notices.desc=Περιγραφή notices.op=Λειτ. notices.delete_success=Οι ειδοποιήσεις του συστήματος έχουν διαγραφεί. + [action] create_repo=δημιούργησε το αποθετήριο <a href="%s">%s</a> rename_repo=μετονόμασε το αποθετήριο από <code>%[1]s</code> σε <a href="%[2]s">%[3]s</a> @@ -3118,6 +3284,7 @@ desc=Διαχείριση πακέτων μητρώου. empty=Δεν υπάρχουν πακέτα ακόμα. empty.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο πακέτων, ανατρέξτε <a target="_blank" rel="noopener noreferrer" href="%s">στην τεκμηρίωση</a>. empty.repo=Μήπως ανεβάσατε ένα πακέτο, αλλά δεν εμφανίζεται εδώ; Πηγαίνετε στις <a href="%[1]s">ρυθμίσεις πακέτων</a> και συνδέστε το σε αυτό το αποθετήριο. +registry.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο %s, ανατρέξτε στη τεκμηρίωση <a target="_blank" rel="noopener noreferrer" href="%s"></a>. filter.type=Τύπος filter.type.all=Όλα filter.no_result=Το φίλτρο δεν παρήγαγε αποτελέσματα. @@ -3203,7 +3370,11 @@ pub.install=Για να εγκαταστήσετε το πακέτο μέσω τ pypi.requires=Απαιτεί Python pypi.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το pip, εκτελέστε την ακόλουθη εντολή: 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=Εξαρτήσεις Εκτέλεσης @@ -3227,14 +3398,17 @@ settings.delete.success=Το πακέτο έχει διαγραφεί. settings.delete.error=Αποτυχία διαγραφής του πακέτου. owner.settings.cargo.title=Ευρετήριο Μητρώου Cargo owner.settings.cargo.initialize=Αρχικοποίηση Ευρετηρίου +owner.settings.cargo.initialize.description=Απαιτείται ένα ειδικό αποθετήριο ευρετηρίου Git για τη χρήση του μητρώου Cargo. Χρησιμοποιώντας αυτή την επιλογή θα δημιουργηθεί ξανά το αποθετήριο και θα ρυθμιστεί αυτόματα. owner.settings.cargo.initialize.error=Αποτυχία αρχικοποίησης ευρετηρίου Cargo: %v owner.settings.cargo.initialize.success=Ο ευρετήριο Cargo δημιουργήθηκε με επιτυχία. owner.settings.cargo.rebuild=Αναδημιουργία Ευρετηρίου +owner.settings.cargo.rebuild.description=Η ανοικοδόμηση μπορεί να είναι χρήσιμη εάν ο δείκτης δεν είναι συγχρονισμένος με τα αποθηκευμένα πακέτα Cargo. owner.settings.cargo.rebuild.error=Αποτυχία αναδόμησης του ευρετηρίου Cargo: %v owner.settings.cargo.rebuild.success=Το ευρετήριο Cargo αναδομήθηκε με επιτυχία. owner.settings.cleanuprules.title=Διαχείριση Κανόνων Εκκαθάρισης owner.settings.cleanuprules.add=Προσθήκη Κανόνα Εκκαθάρισης owner.settings.cleanuprules.edit=Επεξεργασία Κανόνα Εκκαθάρισης +owner.settings.cleanuprules.none=Δεν υπάρχουν διαθέσιμοι κανόνες εκκαθάρισης. Παρακαλούμε συμβουλευτείτε την τεκμηρίωση. owner.settings.cleanuprules.preview=Προεπισκόπηση Κανόνα Εκκαθάρισης owner.settings.cleanuprules.preview.overview=%d πακέτα έχουν προγραμματιστεί να αφαιρεθούν. owner.settings.cleanuprules.preview.none=Ο κανόνας εκκαθάρισης δεν ταιριάζει με κανένα πακέτο. @@ -3253,6 +3427,7 @@ owner.settings.cleanuprules.success.update=Ο κανόνας καθαρισμο owner.settings.cleanuprules.success.delete=Ο κανόνας καθαρισμού διαγράφηκε. owner.settings.chef.title=Μητρώο Chef owner.settings.chef.keypair=Δημιουργία ζεύγους κλειδιών +owner.settings.chef.keypair.description=Ένα ζεύγος κλειδιών είναι απαραίτητο για ταυτοποίηση στο μητρώο Chef. Αν έχετε δημιουργήσει ένα ζεύγος κλειδιών πριν, η δημιουργία ενός νέου ζεύγους κλειδιών θα απορρίψει το παλιό ζεύγος κλειδιών. [secrets] secrets=Μυστικά @@ -3279,6 +3454,7 @@ status.waiting=Αναμονή status.running=Εκτελείται status.success=Επιτυχές status.failure=Αποτυχία +status.cancelled=Ακυρώθηκε status.skipped=Παρακάμφθηκε status.blocked=Αποκλείστηκε @@ -3295,6 +3471,7 @@ runners.labels=Ετικέτες runners.last_online=Τελευταία Ώρα Σύνδεσης runners.runner_title=Εκτελεστής runners.task_list=Πρόσφατες εργασίες στον εκτελεστή +runners.task_list.no_tasks=Δεν υπάρχει καμία εργασία ακόμα. runners.task_list.run=Εκτέλεση runners.task_list.status=Κατάσταση runners.task_list.repository=Αποθετήριο @@ -3315,16 +3492,46 @@ runners.status.idle=Αδρανής runners.status.active=Ενεργό runners.status.offline=Χωρίς Σύνδεση runners.version=Έκδοση +runners.reset_registration_token=Επαναφορά διακριτικού εγγραφής runners.reset_registration_token_success=Επιτυχής επανέκδοση διακριτικού εγγραφής του εκτελεστή runs.all_workflows=Όλες Οι Ροές Εργασίας runs.commit=Υποβολή +runs.scheduled=Προγραμματισμένα +runs.pushed_by=ωθήθηκε από runs.invalid_workflow_helper=Το αρχείο ροής εργασίας δεν είναι έγκυρο. Ελέγξτε το αρχείο σας: %s +runs.no_matching_online_runner_helper=Κανένας δικτυακός δρομέας με ετικέτα: %s +runs.actor=Φορέας runs.status=Κατάσταση +runs.actors_no_select=Όλοι οι φορείς +runs.status_no_select=Όλες οι καταστάσεις +runs.no_results=Δεν βρέθηκαν αποτελέσματα. +runs.no_workflows=Δεν υπάρχουν ροές εργασίας ακόμα. +runs.no_runs=Η ροή εργασίας δεν έχει τρέξει ακόμα. +runs.empty_commit_message=(κενό μήνυμα υποβολής) +workflow.disable=Απενεργοποίηση Ροής Εργασιών +workflow.disable_success=Η ροή εργασίας '%s' απενεργοποιήθηκε επιτυχώς. +workflow.enable=Ενεργοποίηση Ροής Εργασίας +workflow.enable_success=Η ροή εργασίας '%s' ενεργοποιήθηκε επιτυχώς. +workflow.disabled=Η ροή εργασιών είναι απενεργοποιημένη. need_approval_desc=Πρέπει να εγκριθεί η εκτέλεση ροών εργασίας για pull request από fork. +variables=Μεταβλητές +variables.management=Διαχείριση Μεταβλητών +variables.creation=Προσθήκη Μεταβλητής +variables.none=Δεν υπάρχουν μεταβλητές ακόμα. +variables.deletion=Αφαίρεση μεταβλητής +variables.deletion.description=Η αφαίρεση μιας μεταβλητής είναι μόνιμη και δεν μπορεί να αναιρεθεί. Συνέχεια; +variables.description=Η μεταβλητές θα δίνονται σε ορισμένες δράσεις και δεν μπορούν να διαβαστούν αλλιώς. +variables.edit=Επεξεργασία Μεταβλητής +variables.deletion.failed=Αποτυχία αφαίρεσης της μεταβλητής. +variables.deletion.success=Η μεταβλητή έχει αφαιρεθεί. +variables.creation.failed=Αποτυχία προσθήκης μεταβλητής. +variables.creation.success=Η μεταβλητή "%s" έχει προστεθεί. +variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής. +variables.update.success=Η μεταβλητή έχει τροποποιηθεί. [projects] type-1.display_name=Ατομικό Έργο @@ -3332,6 +3539,11 @@ type-2.display_name=Έργο Αποθετηρίου type-3.display_name=Έργο Οργανισμού [git.filemode] +changed_filemode=%[1]s → %[2]s ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", … +directory=Φάκελος +normal_file=Κανονικό αρχείο +executable_file=Εκτελέσιμο αρχείο symbolic_link=Symbolic link +submodule=Υπομονάδα diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9af4d70171..0a3d12d7a4 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -25,6 +25,7 @@ enable_javascript = This website requires JavaScript. toc = Table of Contents licenses = Licenses return_to_gitea = Return to Gitea +more_items = More items username = Username email = Email Address @@ -113,6 +114,7 @@ loading = Loading… error = Error error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it. go_back = Go Back +invalid_data = Invalid data: %v never = Never unknown = Unknown @@ -123,6 +125,7 @@ pin = Pin unpin = Unpin artifacts = Artifacts +confirm_delete_artifact = Are you sure you want to delete the artifact '%s' ? archived = Archived @@ -141,6 +144,43 @@ confirm_delete_selected = Confirm to delete all selected items? name = Name value = Value +filter = Filter +filter.clear = Clear Filter +filter.is_archived = Archived +filter.not_archived = Not Archived +filter.is_fork = Forked +filter.not_fork = Not Forked +filter.is_mirror = Mirrored +filter.not_mirror = Not Mirrored +filter.is_template = Template +filter.not_template = Not Template +filter.public = Public +filter.private = Private + +no_results_found = No results found. + +[search] +search = Search... +type_tooltip = Search type +fuzzy = Fuzzy +fuzzy_tooltip = Include results that also match the search term closely +match = Match +match_tooltip = Include only results that match the exact search term +repo_kind = Search repos... +user_kind = Search users... +org_kind = Search orgs... +team_kind = Search teams... +code_kind = Search code... +code_search_unavailable = Code search is currently not available. Please contact the site administrator. +code_search_by_git_grep = Current code search results are provided by "git grep". There might be better results if site administrator enables Repository Indexer. +package_kind = Search packages... +project_kind = Search projects... +branch_kind = Search branches... +commit_kind = Search commits... +runner_kind = Search runners... +no_results = No matching results found. +keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator. + [aria] navbar = Navigation Bar footer = Footer @@ -246,6 +286,7 @@ email_title = Email Settings smtp_addr = SMTP Host smtp_port = SMTP Port smtp_from = Send Email As +smtp_from_invalid = The "Send Email As" address is invalid smtp_from_helper = Email address Gitea will use. Enter a plain email address or use the "Name" <email@example.com> format. mailer_user = SMTP Username mailer_password = SMTP Password @@ -305,6 +346,7 @@ env_config_keys = Environment Configuration env_config_keys_prompt = The following environment variables will also be applied to your configuration file: [home] +nav_menu = Navigation Menu uname_holder = Username or Email Address password_holder = Password switch_dashboard_context = Switch Dashboard Context @@ -314,7 +356,6 @@ collaborative_repos = Collaborative Repositories my_orgs = My Organizations my_mirrors = My Mirrors view_home = View %s -search_repos = Find a repository… filter = Other Filters filter_by_team_repositories = Filter by team repositories feed_of = Feed of "%s" @@ -335,20 +376,8 @@ issues.in_your_repos = In your repositories repos = Repositories users = Users organizations = Organizations -search = Search go_to = Go to code = Code -search.type.tooltip = Search type -search.fuzzy = Fuzzy -search.fuzzy.tooltip = Include results that also matches the search term closely -search.match = Match -search.match.tooltip = Include only results that matches the exact search term -code_search_unavailable = Currently code search is not available. Please contact your site administrator. -repo_no_results = No matching repositories found. -user_no_results = No matching users found. -org_no_results = No matching organizations found. -code_no_results = No source code matching your search term found. -code_search_results = Search results for "%s" code_last_indexed_at = Last indexed %s relevant_repositories_tooltip = Repositories that are forks or that have no topic, no icon, and no description are hidden. relevant_repositories = Only relevant repositories are being shown, <a href="%s">show unfiltered results</a>. @@ -366,7 +395,7 @@ forgot_password_title= Forgot Password forgot_password = Forgot password? sign_up_now = Need an account? Register now. sign_up_successful = Account was successfully created. Welcome! -confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. +confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it. must_change_password = Update your password allow_password_change = Require user to change password (recommended) reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process. @@ -376,6 +405,7 @@ prohibit_login = Sign In Prohibited prohibit_login_desc = Your account is prohibited from signing in, please contact your site administrator. resent_limit_prompt = You have already requested an activation email recently. Please wait 3 minutes and try again. has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below. +change_unconfirmed_mail_address = If your registration email address is incorrect, you can change it here and resend a new confirmation email. resend_mail = Click here to resend your activation email email_not_associate = The email address is not associated with any account. send_reset_mail = Send Account Recovery Email @@ -556,6 +586,7 @@ team_name_been_taken = The team name is already taken. team_no_units_error = Allow access to at least one repository section. email_been_used = The email address is already used. email_invalid = The email address is invalid. +email_domain_is_not_allowed = The domain of user email <b>%s</b> conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST. Please ensure your operation is expected. openid_been_used = The OpenID address "%s" is already used. username_password_incorrect = Username or password is incorrect. password_complexity = Password does not pass complexity requirements: @@ -567,6 +598,8 @@ enterred_invalid_repo_name = The repository name you entered is incorrect. enterred_invalid_org_name = The organization name you entered is incorrect. enterred_invalid_owner_name = The new owner name is not valid. enterred_invalid_password = The password you entered is incorrect. +unset_password = The login user has not set the password. +unsupported_login_type = The login type is not supported to delete account. user_not_exist = The user does not exist. team_not_exist = The team does not exist. last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization. @@ -616,6 +649,30 @@ form.name_reserved = The username "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username. form.name_chars_not_allowed = User name "%s" contains invalid characters. +block.block = Block +block.block.user = Block user +block.block.org = Block user for organization +block.block.failure = Failed to block user: %s +block.unblock = Unblock +block.unblock.failure = Failed to unblock user: %s +block.blocked = You have blocked this user. +block.title = Block a user +block.info = Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user. +block.info_1 = Blocking a user prevents the following actions on your account and your repositories: +block.info_2 = following your account +block.info_3 = send you notifications by @mentioning your username +block.info_4 = inviting you as a collaborator to their repositories +block.info_5 = starring, forking or watching on repositories +block.info_6 = opening and commenting on issues or pull requests +block.info_7 = reacting on your comments in issues or pull requests +block.user_to_block = User to block +block.note = Note +block.note.title = Optional note: +block.note.info = The note is not visible to the blocked user. +block.note.edit = Edit note +block.list = Blocked users +block.list.none = You have not blocked any users. + [settings] profile = Profile account = Account @@ -760,7 +817,6 @@ gpg_invalid_token_signature = The provided GPG key, signature and token do not m gpg_token_required = You must provide a signature for the below token gpg_token = Token gpg_token_help = You can generate a signature using: -gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature = Armored GPG signature key_signature_gpg_placeholder = Begins with '-----BEGIN PGP SIGNATURE-----' verify_gpg_key_success = GPG key "%s" has been verified. @@ -953,8 +1009,9 @@ fork_visibility_helper = The visibility of a forked repository cannot be changed fork_branch = Branch to be cloned to the fork all_branches = All branches fork_no_valid_owners = This repository can not be forked because there are no valid owners. +fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner. use_template = Use this template -clone_in_vsc = Clone in VS Code +open_with_editor = Open with %s download_zip = Download ZIP download_tar = Download TAR.GZ download_bundle = Download BUNDLE @@ -1007,6 +1064,7 @@ watchers = Watchers stargazers = Stargazers stars_remove_warning = This will remove all stars from this repository. forks = Forks +stars = Stars reactions_more = and %d more unit_disabled = The site administrator has disabled this repository section. language_other = Other @@ -1128,6 +1186,7 @@ watch = Watch unstar = Unstar star = Star fork = Fork +action.blocked_user = Cannot perform action because you are blocked by the repository owner. download_archive = Download Repository more_operations = More Operations @@ -1174,6 +1233,8 @@ file_view_rendered = View Rendered file_view_raw = View Raw file_permalink = Permalink file_too_large = The file is too large to be shown. +code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s +code_preview_line_in = Line %[1]d in %[2]s invisible_runes_header = `This file contains invisible Unicode characters` invisible_runes_description = `This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.` ambiguous_runes_header = `This file contains ambiguous Unicode characters` @@ -1256,6 +1317,8 @@ editor.file_editing_no_longer_exists = The file being edited, "%s", no longer ex editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository. editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them. editor.file_already_exists = A file named "%s" already exists in this repository. +editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge. +editor.push_out_of_date = The push appears to be out of date. editor.commit_empty_file_header = Commit an empty file editor.commit_empty_file_text = The file you're about to commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. @@ -1279,9 +1342,8 @@ commits.desc = Browse source code change history. commits.commits = Commits commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. commits.nothing_to_compare = These branches are equal. -commits.search = Search commits… commits.search.tooltip = You can prefix keywords with "author:", "committer:", "after:", or "before:", e.g. "revert author:Alice before:2019-01-13". -commits.find = Search +commits.search_branch = This Branch commits.search_all = All Branches commits.author = Author commits.message = Message @@ -1332,7 +1394,6 @@ projects.type.basic_kanban = "Basic Kanban" projects.type.bug_triage = "Bug Triage" projects.template.desc = "Template" projects.template.desc_helper = "Select a project template to get started" -projects.type.uncategorized = Uncategorized projects.column.edit = "Edit Column" projects.column.edit_title = "Name" projects.column.new_title = "Name" @@ -1340,10 +1401,8 @@ projects.column.new_submit = "Create Column" projects.column.new = "New Column" projects.column.set_default = "Set Default" projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls" -projects.column.unset_default = "Unset Default" -projects.column.unset_default_desc = "Unset this column as default" projects.column.delete = "Delete Column" -projects.column.deletion_desc = "Deleting a project column moves all related issues to 'Uncategorized'. Continue?" +projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?" projects.column.color = "Color" projects.open = Open projects.close = Close @@ -1378,6 +1437,8 @@ issues.new.assignees = Assignees issues.new.clear_assignees = Clear assignees issues.new.no_assignees = No Assignees issues.new.no_reviewers = No reviewers +issues.new.blocked_user = Cannot create issue because you are blocked by the repository owner. +issues.edit.blocked_user = Cannot edit content because you are blocked by the poster or repository owner. issues.choose.get_started = Get Started issues.choose.open_external_link = Open issues.choose.blank = Default @@ -1455,7 +1516,6 @@ issues.filter_sort.moststars = Most stars issues.filter_sort.feweststars = Fewest stars issues.filter_sort.mostforks = Most forks issues.filter_sort.fewestforks = Fewest forks -issues.keyword_search_unavailable = Searching by keyword is currently not available. Please contact your site administrator. issues.action_open = Open issues.action_close = Close issues.action_label = Label @@ -1493,6 +1553,7 @@ issues.close_comment_issue = Comment and Close issues.reopen_issue = Reopen issues.reopen_comment_issue = Comment and Reopen issues.create_comment = Comment +issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner. issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>` @@ -1691,6 +1752,7 @@ compare.compare_head = compare pulls.desc = Enable pull requests and code reviews. pulls.new = New Pull Request +pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner. pulls.view = View Pull Request pulls.compare_changes = New Pull Request pulls.allow_edits_from_maintainers = Allow edits from maintainers @@ -1707,7 +1769,6 @@ pulls.compare_compare = pull from pulls.switch_comparison_type = Switch comparison type pulls.switch_head_and_base = Switch head and base pulls.filter_branch = Filter branch -pulls.no_results = No results found. pulls.show_all_commits = Show all commits pulls.show_changes_since_your_last_review = Show changes since your last review pulls.showing_only_single_commit = Showing only changes of commit %[1]s @@ -1775,6 +1836,7 @@ pulls.merge_pull_request = Create merge commit pulls.rebase_merge_pull_request = Rebase then fast-forward pulls.rebase_merge_commit_pull_request = Rebase then create merge commit pulls.squash_merge_pull_request = Create squash commit +pulls.fast_forward_only_merge_pull_request = Fast-forward only pulls.merge_manually = Manually merged pulls.merge_commit_id = The merge commit ID pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed @@ -1788,9 +1850,9 @@ pulls.unrelated_histories = Merge Failed: The merge head and base do not share a pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch. -pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository. +pulls.push_rejected = Push Failed: The push was rejected. Review the Git Hooks for this repository. pulls.push_rejected_summary = Full Rejection Message -pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the Git Hooks for this repository +pulls.push_rejected_no_message = Push Failed: The push was rejected but there was no remote message. Review the Git Hooks for this repository pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.` pulls.status_checking = Some checks are pending pulls.status_checks_success = All checks were successful @@ -1911,6 +1973,10 @@ wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: ' wiki.original_git_entry_tooltip = View original Git file instead of using friendly link. activity = Activity +activity.navbar.pulse = Pulse +activity.navbar.code_frequency = Code Frequency +activity.navbar.contributors = Contributors +activity.navbar.recent_commits = Recent Commits activity.period.filter_label = Period: activity.period.daily = 1 day activity.period.halfweekly = 3 days @@ -1976,16 +2042,10 @@ activity.git_stats_and_deletions = and activity.git_stats_deletion_1 = %d deletion activity.git_stats_deletion_n = %d deletions -search = Search -search.search_repo = Search repository -search.type.tooltip = Search type -search.fuzzy = Fuzzy -search.fuzzy.tooltip = Include results that also matches the search term closely -search.match = Match -search.match.tooltip = Include only results that matches the exact search term -search.results = Search results for "%s" in <a href="%s">%s</a> -search.code_no_results = No source code matching your search term found. -search.code_search_unavailable = Currently code search is not available. Please contact your site administrator. +contributors.contribution_type.filter_label = Contribution type: +contributors.contribution_type.commits = Commits +contributors.contribution_type.additions = Additions +contributors.contribution_type.deletions = Deletions settings = Settings settings.desc = Settings is where you can manage the settings for the repository @@ -2035,6 +2095,8 @@ settings.branches.add_new_rule = Add New Rule settings.advanced_settings = Advanced Settings settings.wiki_desc = Enable Repository Wiki settings.use_internal_wiki = Use Built-In Wiki +settings.default_wiki_branch_name = Default Wiki Branch Name +settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch. settings.use_external_wiki = Use External Wiki settings.external_wiki_url = External Wiki URL settings.external_wiki_url_error = The external wiki URL is not a valid URL. @@ -2064,7 +2126,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default settings.releases_desc = Enable Repository Releases settings.packages_desc = Enable Repository Packages Registry -settings.projects_desc = Enable Repository Projects +settings.projects_desc = Enable Projects +settings.projects_mode_desc = Projects Mode (which kinds of projects to show) +settings.projects_mode_repo = Repo projects only +settings.projects_mode_owner = Only user or org projects +settings.projects_mode_all = All projects settings.actions_desc = Enable Repository Actions settings.admin_settings = Administrator Settings settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) @@ -2090,6 +2156,7 @@ settings.convert_fork_succeed = The fork has been converted into a regular repos settings.transfer = Transfer Ownership settings.transfer.rejected = Repository transfer was rejected. settings.transfer.success = Repository transfer was successful. +settings.transfer.blocked_user = Cannot transfer repository because you are blocked by the new owner. settings.transfer_abort = Cancel transfer settings.transfer_abort_invalid = You cannot cancel a non existent repository transfer. settings.transfer_abort_success = The repository transfer to %s was successfully canceled. @@ -2135,11 +2202,11 @@ settings.add_collaborator_success = The collaborator has been added. settings.add_collaborator_inactive_user = Cannot add an inactive user as a collaborator. settings.add_collaborator_owner = Cannot add an owner as a collaborator. settings.add_collaborator_duplicate = The collaborator is already added to this repository. +settings.add_collaborator.blocked_user = The collaborator is blocked by the repository owner or vice versa. settings.delete_collaborator = Remove settings.collaborator_deletion = Remove Collaborator settings.collaborator_deletion_desc = Removing a collaborator will revoke their access to this repository. Continue? settings.remove_collaborator_success = The collaborator has been removed. -settings.search_user_placeholder = Search user… settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator. settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner settings.team_not_in_organization = The team is not in the same organization as the repository @@ -2147,7 +2214,6 @@ settings.teams = Teams settings.add_team = Add Team settings.add_team_duplicate = Team already has the repository settings.add_team_success = The team now have access to the repository. -settings.search_team = Search Team… settings.change_team_permission_tip = Team's permission is set on the team setting page and can't be changed per repository settings.delete_team_tip = This team has access to all repositories and can't be removed settings.remove_team_success = The team's access to the repository has been removed. @@ -2300,9 +2366,7 @@ settings.protect_whitelist_committers = Whitelist Restricted Push settings.protect_whitelist_committers_desc = Only whitelisted users or teams will be allowed to push to this branch (but not force push). settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push. settings.protect_whitelist_users = Whitelisted users for pushing: -settings.protect_whitelist_search_users = Search users… settings.protect_whitelist_teams = Whitelisted teams for pushing: -settings.protect_whitelist_search_teams = Search teams… settings.protect_merge_whitelist_committers = Enable Merge Whitelist settings.protect_merge_whitelist_committers_desc = Allow only whitelisted users or teams to merge pull requests into this branch. settings.protect_merge_whitelist_users = Whitelisted users for merging: @@ -2547,7 +2611,6 @@ branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be branch.restore = Restore Branch "%s" branch.download = Download Branch "%s" branch.rename = Rename Branch "%s" -branch.search = Search Branch branch.included_desc = This branch is part of the default branch branch.included = Included branch.create_new_branch = Create branch from branch: @@ -2578,6 +2641,16 @@ find_file.no_matching = No matching file found error.csv.too_large = Can't render this file because it is too large. error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d. error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d. +error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the <a target="_blank" rel="noreferrer" href="%s">documentation</a> to fix them, then push some commits to refresh the status. + +[graphs] +component_loading = Loading %s... +component_loading_failed = Could not load %s +component_loading_info = This might take a bit… +component_failed_to_load = An unexpected error happened. +code_frequency.what = code frequency +contributors.what = contributions +recent_commits.what = recent commits [org] org_name_holder = Organization Name @@ -2683,7 +2756,6 @@ teams.write_permission_desc = This team grants <strong>Write</strong> access: me teams.admin_permission_desc = This team grants <strong>Admin</strong> access: members can read from, push to and add collaborators to team repositories. teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization. teams.repositories = Team Repositories -teams.search_repo_placeholder = Search repository… teams.remove_all_repos_title = Remove all team repositories teams.remove_all_repos_desc = This will remove all repositories from the team. teams.add_all_repos_title = Add all repositories @@ -2692,6 +2764,7 @@ teams.add_nonexistent_repo = "The repository you're trying to add doesn't exist, teams.add_duplicate_users = User is already a team member. teams.repos.none = No repositories could be accessed by this team. teams.members.none = No members on this team. +teams.members.blocked_user = Cannot add the user because it is blocked by the organization. teams.specific_repositories = Specific repositories teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>. teams.all_repositories = All repositories @@ -2704,6 +2777,7 @@ teams.invite.by = Invited by %s teams.invite.description = Please click the button below to join the team. [admin] +maintenance = Maintenance dashboard = Dashboard self_check = Self Check identity_access = Identity & Access @@ -2716,6 +2790,8 @@ integrations = Integrations authentication = Authentication Sources emails = User Emails config = Configuration +config_summary = Summary +config_settings = Settings notices = System Notices monitor = Monitoring first_page = First @@ -2725,7 +2801,7 @@ settings = Admin Settings dashboard.new_version_hint = Gitea %s is now available, you are running %s. Check <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">the blog</a> for more details. dashboard.statistic = Summary -dashboard.operations = Maintenance Operations +dashboard.maintenance_operations = Maintenance Operations dashboard.system_status = System Status dashboard.operation_name = Operation Name dashboard.operation_switch = Switch @@ -2892,9 +2968,6 @@ repos.unadopted.no_more = No more unadopted repositories found repos.owner = Owner repos.name = Name repos.private = Private -repos.watches = Watches -repos.stars = Stars -repos.forks = Forks repos.issues = Issues repos.size = Size repos.lfs_size = LFS Size @@ -3019,7 +3092,7 @@ auths.tip.nextcloud = Register a new OAuth consumer on your instance using the f auths.tip.dropbox = Create a new application at https://www.dropbox.com/developers/apps auths.tip.facebook = Register a new application at https://developers.facebook.com/apps and add the product "Facebook Login" auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new -auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications +auths.tip.gitlab_new = Register a new application on https://gitlab.com/-/profile/applications auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at https://console.developers.google.com/ auths.tip.openid_connect = Use the OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) to specify the endpoints auths.tip.twitter = Go to https://dev.twitter.com/apps, create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled @@ -3155,6 +3228,7 @@ config.picture_config = Picture and Avatar Configuration config.picture_service = Picture Service config.disable_gravatar = Disable Gravatar config.enable_federated_avatar = Enable Federated Avatars +config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default. config.git_config = Git Configuration config.git_disable_diff_highlight = Disable Diff Syntax Highlight @@ -3234,6 +3308,7 @@ notices.op = Op. notices.delete_success = The system notices have been deleted. self_check.no_problem_found = No problem found yet. +self_check.startup_warnings = Startup warnings: self_check.database_collation_mismatch = Expect database to use collation: %s self_check.database_collation_case_insensitive = Database is using a collation %s, which is an insensitive collation. Although Gitea could work with it, there might be some rare cases which don't work as expected. self_check.database_inconsistent_collation_columns = Database is using collation %s, but these columns are using mismatched collations. It might cause some unexpected problems. @@ -3553,6 +3628,7 @@ runs.scheduled = Scheduled runs.pushed_by = pushed by runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s runs.no_matching_online_runner_helper = No matching online runner with label: %s +runs.no_job_without_needs = The workflow must contain at least one job without dependencies. runs.actor = Actor runs.status = Status runs.actors_no_select = All actors diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 1a82ce5b76..fc78e1d439 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -139,6 +139,15 @@ confirm_delete_selected=¿Borrar todos los elementos seleccionados? name=Nombre value=Valor +filter=Filtro +filter.is_archived=Archivado +filter.is_template=Plantilla +filter.public=Público +filter.private=Privado + + +[search] + [aria] navbar=Barra de navegación footer=Pie @@ -312,7 +321,6 @@ collaborative_repos=Repositorios colaborativos my_orgs=Mis organizaciones my_mirrors=Mis réplicas view_home=Ver %s -search_repos=Buscar un repositorio… filter=Otros filtros filter_by_team_repositories=Filtrar por repositorios de equipo feed_of=`Suministro de noticias de "%s"` @@ -333,20 +341,8 @@ issues.in_your_repos=En tus repositorios repos=Repositorios users=Usuarios organizations=Organizaciones -search=Buscar go_to=Ir a code=Código -search.type.tooltip=Tipo de búsqueda -search.fuzzy=Parcial -search.fuzzy.tooltip=Incluye los resultados que también coincidan con el término de búsqueda -search.match=Coincidir -search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto -code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio. -repo_no_results=No se ha encontrado ningún repositorio coincidente. -user_no_results=No se ha encontrado ningún usuario coincidente. -org_no_results=No se ha encontrado ninguna organización coincidente. -code_no_results=No se ha encontrado código de fuente que coincida con su término de búsqueda. -code_search_results=Resultados de búsqueda para «%s» code_last_indexed_at=Indexado por última vez %s relevant_repositories_tooltip=Repositorios que son bifurcaciones o que no tienen ningún tema, ningún icono, y ninguna descripción están ocultos. relevant_repositories=Solo se muestran repositorios relevantes, <a href="%s">mostrar resultados sin filtrar</a>. @@ -363,7 +359,6 @@ forgot_password_title=He olvidado mi contraseña forgot_password=¿Has olvidado tu contraseña? sign_up_now=¿Necesitas una cuenta? Regístrate ahora. sign_up_successful=La cuenta se ha creado correctamente. ¡Bienvenido! -confirmation_mail_sent_prompt=Un nuevo correo de confirmación se ha enviado a <b>%s</b>. Comprueba tu bandeja de entrada en las siguientes %s para completar el registro. must_change_password=Actualizar su contraseña allow_password_change=Obligar al usuario a cambiar la contraseña (recomendado) reset_password_mail_sent_prompt=Un correo de confirmación se ha enviado a <b>%s</b>. Compruebe su bandeja de entrada en las siguientes %s para completar el proceso de recuperación de la cuenta. @@ -585,6 +580,7 @@ org_still_own_packages=Esta organización todavía posee uno o más paquetes, el target_branch_not_exist=La rama de destino no existe + [user] change_avatar=Cambiar su avatar… joined_on=Se unió el %s @@ -610,6 +606,7 @@ form.name_reserved=El nombre de usuario "%s" está reservado. form.name_pattern_not_allowed=El patrón "%s" no está permitido en un nombre de usuario. form.name_chars_not_allowed=El nombre de usuario "%s" contiene caracteres no válidos. + [settings] profile=Perfil account=Cuenta @@ -754,7 +751,6 @@ gpg_invalid_token_signature=La clave GPG proporcionada, la firma y el token no c gpg_token_required=Debe proporcionar una firma para el token de abajo gpg_token=Token gpg_token_help=Puede generar una firma de la siguiente manera: -gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Firma GPG armadura key_signature_gpg_placeholder=Comienza con '-----BEGIN PGP SIGNATURE-----' verify_gpg_key_success=La clave GPG "%s" ha sido verificada. @@ -944,7 +940,6 @@ fork_branch=Rama a clonar en la bifurcación all_branches=Todas las ramas fork_no_valid_owners=Este repositorio no puede ser bifurcado porque no hay propietarios válidos. use_template=Utilizar esta plantilla -clone_in_vsc=Clonar en VS Code download_zip=Descargar ZIP download_tar=Descargar TAR.GZ download_bundle=Descargar BUNDLE @@ -1263,9 +1258,7 @@ commits.desc=Ver el historial de cambios de código fuente. commits.commits=Commits commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes. commits.nothing_to_compare=Estas ramas son iguales. -commits.search=Buscar commits… commits.search.tooltip=Puede prefijar palabras clave con "author:", "committer:", "after:", o "before:", p. ej., "revertir author:Alice before:2019-01-13". -commits.find=Buscar commits.search_all=Todas las Ramas commits.author=Autor commits.message=Mensaje @@ -1316,7 +1309,6 @@ projects.type.basic_kanban=Kanban básico projects.type.bug_triage=Prueba de error projects.template.desc=Plantilla del proyecto projects.template.desc_helper=Seleccione una plantilla de proyecto para empezar -projects.type.uncategorized=Sin categorizar projects.column.edit=Editar columna projects.column.edit_title=Nombre projects.column.new_title=Nombre @@ -1324,10 +1316,7 @@ projects.column.new_submit=Crear columna projects.column.new=Nueva columna projects.column.set_default=Establecer como predeterminado projects.column.set_default_desc=Establecer esta columna como predeterminada para incidencias no categorizadas y pulls -projects.column.unset_default=Anular valor predeterminado -projects.column.unset_default_desc=Anular esta columna como la predeterminada projects.column.delete=Borrar columna -projects.column.deletion_desc=Eliminar una columna del proyecto mueve todos los problemas relacionados a 'Sin categorizar'. ¿Continuar? projects.column.color=Color projects.open=Abrir projects.close=Cerrar @@ -1439,7 +1428,6 @@ issues.filter_sort.moststars=Mas estrellas issues.filter_sort.feweststars=Menor número de estrellas issues.filter_sort.mostforks=La mayoría de forks issues.filter_sort.fewestforks=Menor número de forks -issues.keyword_search_unavailable=La búsqueda por palabra clave no está disponible actualmente. Por favor, contacte con el administrador de su sitio. issues.action_open=Abrir issues.action_close=Cerrar issues.action_label=Etiqueta @@ -1691,7 +1679,6 @@ pulls.compare_compare=recuperar de pulls.switch_comparison_type=Cambiar tipo de comparación pulls.switch_head_and_base=Intercambiar cabeza y base pulls.filter_branch=Filtrar rama -pulls.no_results=Sin resultados. pulls.show_all_commits=Mostrar todos los commits pulls.show_changes_since_your_last_review=Mostrar cambios desde tu última revisión pulls.showing_only_single_commit=Mostrando solo los cambios del commit %[1]s @@ -1952,16 +1939,7 @@ activity.git_stats_and_deletions=y activity.git_stats_deletion_1=%d eliminación activity.git_stats_deletion_n=%d eliminaciones -search=Buscar -search.search_repo=Buscar repositorio -search.type.tooltip=Tipo de búsqueda -search.fuzzy=Parcial -search.fuzzy.tooltip=Incluye los resultados que también coinciden aproximadamente con el término de búsqueda -search.match=Coincidir -search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto -search.results=Resultados de la búsqueda para "%s" en <a href="%s">%s</a> -search.code_no_results=No se ha encontrado código de fuente que coincida con su término de búsqueda. -search.code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio. +contributors.contribution_type.commits=Commits settings=Configuración settings.desc=La configuración es donde puede administrar la configuración del repositorio @@ -2040,6 +2018,7 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir ediciones de manten settings.releases_desc=Activar lanzamientos del repositorio settings.packages_desc=Habilitar registro de paquetes de repositorio settings.projects_desc=Activar Proyectos de Repositorio +settings.projects_mode_all=Todos los proyectos settings.actions_desc=Activar Acciones del repositorio settings.admin_settings=Ajustes de administrador settings.admin_enable_health_check=Activar cheques de estado de salud del repositorio (git fsck) @@ -2114,7 +2093,6 @@ settings.delete_collaborator=Eliminar settings.collaborator_deletion=Eliminar colaborador settings.collaborator_deletion_desc=Eliminar un colaborador revocará su acceso a este repositorio. ¿Continuar? settings.remove_collaborator_success=El colaborador ha sido eliminado. -settings.search_user_placeholder=Buscar usuario… settings.org_not_allowed_to_be_collaborator=Las organizaciones no pueden ser añadidas como colaboradoras. settings.change_team_access_not_allowed=Cambiar el acceso del equipo al repositorio se ha restringido al propietario de la organización settings.team_not_in_organization=El equipo no pertenece a la misma organización que el repositorio @@ -2122,7 +2100,6 @@ settings.teams=Equipos settings.add_team=Añadir equipo settings.add_team_duplicate=El equipo ya tiene acceso al repositorio settings.add_team_success=Ahora el equipo ya tiene acceso al repositorio. -settings.search_team=Buscar equipos… settings.change_team_permission_tip=El permiso del equipo está establecido en la página de configuración del equipo y no puede ser cambiado por repositorio settings.delete_team_tip=Este equipo tiene acceso a todos los repositorios y no puede ser eliminado settings.remove_team_success=Se ha eliminado el acceso del equipo al repositorio. @@ -2275,9 +2252,7 @@ settings.protect_whitelist_committers=Hacer push restringido a la lista blanca settings.protect_whitelist_committers_desc=Sólo se permitirá a los usuarios o equipos de la lista blanca hacer push a esta rama (pero no forzar push). settings.protect_whitelist_deploy_keys=Lista blanca de claves de despliegue con acceso de escritura a push. settings.protect_whitelist_users=Usuarios en la lista blanca para hacer push: -settings.protect_whitelist_search_users=Buscar usuarios… settings.protect_whitelist_teams=Equipos en la lista blanca para hacer push: -settings.protect_whitelist_search_teams=Buscar equipos… settings.protect_merge_whitelist_committers=Activar lista blanca para fusionar settings.protect_merge_whitelist_committers_desc=Permitir a los usuarios o equipos de la lista a fusionar peticiones pull dentro de esta rama. settings.protect_merge_whitelist_users=Usuarios en la lista blanca para fusionar: @@ -2518,7 +2493,6 @@ branch.default_deletion_failed=La rama "%s" es la rama por defecto. No se puede branch.restore=`Restaurar rama "%s"` branch.download=`Descargar rama "%s"` branch.rename=`Renombrar rama "%s"` -branch.search=Buscar rama branch.included_desc=Esta rama forma parte de la predeterminada branch.included=Incluida branch.create_new_branch=Crear rama desde la rama: @@ -2550,6 +2524,8 @@ error.csv.too_large=No se puede renderizar este archivo porque es demasiado gran error.csv.unexpected=No se puede procesar este archivo porque contiene un carácter inesperado en la línea %d y la columna %d. error.csv.invalid_field_count=No se puede procesar este archivo porque tiene un número incorrecto de campos en la línea %d. +[graphs] + [org] org_name_holder=Nombre de la organización org_full_name_holder=Nombre completo de la organización @@ -2654,7 +2630,6 @@ teams.write_permission_desc=Este equipo tiene permisos de <strong>Escritura</str teams.admin_permission_desc=Este equipo tiene permisos de <strong>Administración</strong>: 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 <strong>Crear repositorio</strong>: 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 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">el blog</a> 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 (<server>/.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 <a href="%s">%s</a> rename_repo=repositorio renombrado de <code>%[1]s</code> a <a href="%[2]s">%[3]s</a> @@ -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=ایمیل تاییدیه جدیدی به <b>%s</b> ارسال شد. لطفا صندوق ورودی خود را در %d ساعت آینده برای تکمیل فرایند ثبت نام بررسی کنید. must_change_password=گذرواژه خود را به روز کنید allow_password_change=نیاز به کاربر برای تغییرگذرواژه (توصیه می شود) reset_password_mail_sent_prompt=ایمیل تاییدیه جدیدی به <b>%s</b> ارسال شد. لطفا صندوق ورودی خود را در %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 بین این شاخه ها از قبل وجود دارد: <a href="%[1]s">%[2]s#%[3]d</a>` @@ -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" در <a href="%s">%s</a> -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=این تیم دسترسی <strong>نوشتن</stron teams.admin_permission_desc=این تیم دسترسی <strong>نوشتن</strong> خواهد داشت: اعضا خواهند توانست مخازن تیم را خوانده ، تغییراتی در آنها اعمال کرده و یا همکارانشان را به مخازن اضافه نمایند. teams.create_repo_permission_desc=علاوه بر این ، این تیم اجازه <strong> ساخت مخزن </strong> دسترسی : اعضا می توانند مخازن جدیدی را در سازمان ایجاد کنند. 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 (<server> /.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=مخزن ایجاد شده <a href="%s"> %s</a> rename_repo=مخزن تغییر نام داد از <code>%[1]s</code> به <a href="%[2]s">%[3]s</a> 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 <b>%s</b>, 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 <b>%s</b>. 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: <a href="%[1]s">%[2]s#%[3]d</a>` @@ -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 <strong>EI VOI</strong> 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 '<b>%s</b>' 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 <a href="%s">%s</a> rename_repo=uudelleennimetty repo <code>%[1]s</code> nimelle <a href="%[2]s">%[3]s</a> 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, <a href="%s">afficher les résultats non filtrés</a>. @@ -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é à <b>%s</b>. 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é à <b>%s</b>. 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 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">des mots de passe ayant fuité</a> 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 <a %s>routes API</a> correspondantes. Lisez la <a %s>documentation</a> 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 <a target="_blank" rel="noopener noreferrer" href="%s">choisir une licence</a>. +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 : <a href="%[1]s">%[2]s#%[3]d</a>' 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 <a href="%s"> %s</a> -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 <strong>écriture</ teams.admin_permission_desc=Cette équipe permet l'accès <strong>administrateur</strong> : 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 <strong>Créer un dépôt</strong> : 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 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> 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 (<server>/.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 <a href="%s">%s</a> rename_repo=a rebaptisé le dépôt <s><code>%[1]s</code></s> en <a href="%[2]s">%[3]s</a> @@ -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 <a target="_blank" rel="noopener noreferrer" href="%s">le guide de démarrage rapide</a>. -runs.no_workflows.documentation=Pour plus d’informations sur les Actions Gitea, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>. +runs.no_workflows.quick_start=Vous découvrez les Actions Gitea ? Consultez <a target="_blank" rel="noopener noreferrer" href="%s">le didacticiel</a>. +runs.no_workflows.documentation=Pour plus d’informations sur les actions Gitea, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>. 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: <b>%s</b>. 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: <b>%s</b>. 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) <code>%[2]s</code>-ból <code id="branch_target">%[3]s</code>-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: <a href="%s">%s</a>` -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 <strong>Olvasási</strong> 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 (<kiszolgáló>/.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: <a href="%s"> %s</a> rename_repo=átnevezte a(z) <code>%[1]s</code> tárolót <a href="%[2]s">%[3]s</a>-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 <b>%s</b>. 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 <b>%s</b>. 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 <code>%[2]s</code> menuju <code id="branch_target">%[3]s</code> pulls.merged_title_desc=commit %[1]d telah digabungkan dari <code>%[2]s</code> menjadi <code>%[3]s</code> %[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 <a href="%s">%s</a> -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 <strong>TIDAK BISA</strong> 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 (<server>/.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 <a href="%s">%s</a> rename_repo=ganti nama gudang penyimpanan dari <code>%[1]s</code> ke <a href="%[2]s">%[3]s</a> @@ -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 <a target="_blank" rel="noopener noreferrer" href="%s">panduan cepat</a>. +runs.no_workflows.documentation=Untuk informasi lebih lanjut tentang Gitea Actions, lihat <a target="_blank" rel="noopener noreferrer" href="%s">dokumentasi</a>. +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 <a href="%[1]s">%[3]s#%[2]s</a>` reopen_issue=`enduropnaði vandamál <a href="%[1]s">%[3]s#%[2]s</a>` 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 <b>%s</b>. 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 <b>%s</b>. 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à: <a href="%[1]s">%[2]s#%[3]d</a>` @@ -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 <a href="%s">%s</a> -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 <strong>Scrittura</ teams.admin_permission_desc=Questo team concede l'accesso di <strong>Amministratore</strong>: 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 <strong>Creare repository</strong>: 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 (<server>/.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 <a href="%s">%s</a> rename_repo=repository rinominato da <code>%[1]s</code> a <a href="%[2]s">[3]s</a> 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=アクセスしようとしたページは<strong>存在しない</strong>か、閲覧が<strong>許可されていません</strong>。 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が使用するメールアドレス。 メールアドレスのみ、または、 "名前" <email@example.com> の形式で入力してください。 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=妥当と思われるリポジトリのみを表示しています。 <a href="%s">フィルタリングしない結果を表示</a>。 @@ -366,7 +394,6 @@ forgot_password_title=パスワードを忘れた forgot_password=パスワードをお忘れですか? sign_up_now=アカウントが必要ですか? 今すぐ登録しましょう。 sign_up_successful=アカウントは無事に作成されました。ようこそ! -confirmation_mail_sent_prompt=<b>%s</b> に確認メールを送信しました。 %s以内に受信トレイを確認し、登録手続きを完了してください。 must_change_password=パスワードの更新 allow_password_change=ユーザーはパスワードの変更が必要 (推奨) reset_password_mail_sent_prompt=<b>%s</b> に確認メールを送信しました。 %s以内に受信トレイを確認し、アカウント回復手続きを完了してください。 @@ -423,6 +450,7 @@ authorization_failed_desc=無効なリクエストを検出したため認可が sspi_auth_failed=SSPI認証に失敗しました password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した<a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">盗まれたパスワードのリスト</a>に含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。 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=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。 +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=あなたが編集を開始したあと、ファイルの内容が変更されました。 <a target="_blank" rel="noopener noreferrer" href="%s">ここをクリック</a>して何が変更されたか確認するか、<strong>もう一度"変更をコミット"をクリック</strong>して上書きします。 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=`同じブランチのプルリクエストはすでに存在します: <a href="%[1]s">%[2]s#%[3]d</a>` 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=<a href="%[2]s">%[3]s</a> 内での "%[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=このチームは<strong>書き込み</strong>ア teams.admin_permission_desc=このチームは<strong>管理者</strong>アクセス権を持ちます: メンバーはチームリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。 teams.create_repo_permission_desc=さらに、このチームには<strong>リポジトリの作成</strong>権限が与えられています: メンバーは組織のリポジトリを新たに作成できます。 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=メンバーは、明示的にチームへ追加したリポジトリにのみアクセスできます。 これを選択しても、すでに<i>すべてのリポジトリ</i>で追加されたリポジトリは自動的に除去<strong>されません</strong>。 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 です。 詳細は <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">ブログ</a> を確認してください。 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はデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。 +defaulthooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookはデフォルトとなり、全ての新規リポジトリにコピーされます。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。 defaulthooks.add_webhook=デフォルトWebhookの追加 defaulthooks.update_webhook=デフォルトWebhookの更新 systemhooks=システムWebhook -systemhooks.desc=Webhookは、特定のGiteaイベントのトリガーが発生した際に、自動的にHTTP POSTリクエストをサーバーへ送信するものです。 ここで定義したWebhookはシステム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。 +systemhooks.desc=Webhookは、特定のGiteaイベントが発生したときに、サーバーにHTTP POSTリクエストを自動的に送信するものです。 ここで定義したWebhookは、システム内のすべてのリポジトリで呼び出されます。 そのため、パフォーマンスに及ぼす影響を考慮したうえで設定してください。 詳しくは<a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">Webhooksガイド</a>をご覧下さい。 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 (<server>/.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=がリポジトリ <a href="%s">%s</a> を作成しました rename_repo=がリポジトリ名を <code>%[1]s</code> から <a href="%[2]s">%[3]s</a> へ変更しました @@ -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=パッケージレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。 -empty.repo=パッケージはアップロードしたけども、ここに表示されない? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。 +empty.repo=パッケージはアップロード済みで、ここに表示されていないですか? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。 registry.documentation=%sレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。 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 の始め方がわからない? <a target="_blank" rel="noopener noreferrer" href="%s">クイックスタートガイド</a>をご覧ください。 -runs.no_workflows.documentation=Gitea Action の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。 +runs.no_workflows.quick_start=Gitea Actions の始め方がわからない? では<a target="_blank" rel="noopener noreferrer" href="%s">クイックスタートガイド</a>をご覧ください。 +runs.no_workflows.documentation=Gitea Actions の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。 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=새로운 확인 메일이 <b>%s</b>로 전송되었습니다. 받은 편지함으로 도착한 메일을 %s 안에 확인해서 등록 절차를 완료하십시오. must_change_password=비밀번호를 변경하세요. allow_password_change=사용자에게 비밀번호 변경을 요청 (권장됨) reset_password_mail_sent_prompt=확인 메일이 <b>%s</b>로 전송되었습니다. 받은 편지함으로 도착한 메일을 %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="<code>%[2]s</code> 에서 <code id=\"branch_target\">%[3]s</code> 로 %[1]d commits 를 머지하려 합니다" pulls.merged_title_desc=<code>%[2]s</code> 에서 <code>%[3]s</code> 로 %[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="<a href=\"%s\">%s</a> 에서 \"%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='<b>%s</b>' 브랜치 보호 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=저장소를 만들었습니다. <a href="%s">%s</a> rename_repo=<code>%[1]s에서</code>에서 <a href="%[2]s"> %[3]s</a>으로 저장소 이름을 바꾸었습니다. 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, <strong>neeksistē</strong> vai arī <strong>Jums nav tiesības</strong> 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 <a href="https://github.com/go-gitea/gitea/issues" target="_blank">GitHub</a>, 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 <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">jāpalaiž izpildāmais fails</a> vajadzīgajai platformai, jāizmanto <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, vai jāiegūst <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">pakotne</a>. platform=Pieejama dažādām platformām platform_desc=Gitea iespējams uzstādīt jebkur, kam <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> 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, <a href="%s">pārādīt nefiltrētus rezultātus</a>. @@ -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 <b>%s</b>, 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 <b>%s</b>. 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 <a href="%s">šeit</a>, 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 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">nozagto paroļu sarakstā</a>, 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 <b>%[1]s</b>, esat reģistrējies %[2]s! activate_account.text_2=Nospiediet uz saites, lai aktivizētu savu kontu lapā <b>%s</b>: 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ā <b>%s</b>: 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 <a %s>API</a> izsaukumiem. Sīkāka informācija pieejama <a %s>dokumentācijā</a>. 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 <strong>ieslēgta</strong> 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 <strong>visos repozitorijos</strong>, 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 <strong>NEATGRIEZENISKA</strong>. @@ -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? <a href="%s">Pārnest repozitoriju.</a> 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ē <a target="_blank" rel="noopener noreferrer" href="%s">palīdzības</a> 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 <a href="%s">.git-blame-ignore-revs</a>. Nospiediet <a href="%s">šeit, lai to apietu</a> un redzētu visu izmaiņu skatu. +blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no <a href="%s">.git-blam-ignore-revs</a>. 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 <a target="_blank" rel="noopener noreferrer" href="%s">noteiktu LFS serveri</a>. 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 <b>%s</b> %s` issues.filter_label=Etiķete issues.filter_label_exclude=`Izmantojiet <code>alt</code> + <code>peles klikšķis vai enter</code>, 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 <a href="#%s">%s</a>` 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=`<a href="%[3]s">aizvēra problēmu %[4]s</a> <a id="%[1] issues.ref_reopened_from=`<a href="%[3]s">atkārtoti atvēra problēmu %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` 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 <code>grupa/nosaukums</code>, lai grupētu etiķētes un varētu norādīt tās kā ekskluzīvas ar citām <code>grupa/</code> 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 <a href="%s" issues.stop_tracking=Apturēt taimeri issues.stop_tracking_history=` beidza strādāt %s` issues.cancel_tracking=Atmest +issues.cancel_tracking_history=`atcēla laika uzskaiti %s` issues.add_time=Manuāli pievienot laiku issues.del_time=Dzēst šo laika žurnāla ierakstu issues.add_time_short=Pievienot laiku @@ -1508,6 +1590,7 @@ issues.due_date_form=dd.mm.yyyy issues.due_date_form_add=Pievienot izpildes termiņu issues.due_date_form_edit=Labot issues.due_date_form_remove=Noņemt +issues.due_date_not_writer=Ir nepieciešama rakstīšanas piekļuve šim repozitorijam, lai varētu mainīt problēmas plānoto izpildes datumu. issues.due_date_not_set=Izpildes termiņš nav uzstādīts. issues.due_date_added=pievienoja izpildes termiņu %s %s issues.due_date_modified=mainīja termiņa datumu no %[2]s uz %[1]s %[3]s @@ -1563,6 +1646,9 @@ issues.review.pending.tooltip=Šis komentārs nav redzams citiem lietotājiem. L issues.review.review=Recenzija issues.review.reviewers=Recenzenti issues.review.outdated=Novecojis +issues.review.outdated_description=Saturs ir mainījies kopš šī komentāra pievienošanas +issues.review.option.show_outdated_comments=Rādīt novecojušus komentārus +issues.review.option.hide_outdated_comments=Paslēpt novecojušus komentārus issues.review.show_outdated=Rādīt novecojušu issues.review.hide_outdated=Paslēpt novecojušu issues.review.show_resolved=Rādīt atrisināto @@ -1601,7 +1687,13 @@ pulls.compare_compare=salīdzināmais pulls.switch_comparison_type=Mainīt salīdzināšanas tipu pulls.switch_head_and_base=Mainīt galvas un pamata atzarus pulls.filter_branch=Filtrēt atzarus -pulls.no_results=Nekas netika atrasts. +pulls.show_all_commits=Rādīt visas revīzijas +pulls.show_changes_since_your_last_review=Rādīt izmaiņas kopš Tavas pēdējās recenzijas +pulls.showing_only_single_commit=Rāda tikai revīzijas %[1]s izmaiņas +pulls.showing_specified_commit_range=Rāda tikai izmaiņas starp %[1]s..%[2]s +pulls.select_commit_hold_shift_for_range=Atlasīt revīziju. Jātur Shift + klikšķis, lai atlasītu vairākas +pulls.review_only_possible_for_full_diff=Recenzēšana ir iespējama tikai tad, kad tiek apskatīts pilns salīdzinājums +pulls.filter_changes_by_commit=Atlasīt pēc revīzijas pulls.nothing_to_compare=Nav ko salīdzināt, jo bāzes un salīdzināmie atzari ir vienādi. pulls.nothing_to_compare_and_allow_empty_pr=Šie atzari ir vienādi. Izveidotais izmaiņu pieprasījums būs tukšs. pulls.has_pull_request=`Izmaiņu pieprasījums starp šiem atzariem jau eksistē: <a href="%[1]s">%[2]s#%[3]d</a>` @@ -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.<br>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 <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at=`atkārtoti atvēra šo izmaiņu pieprasījumu <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.cmd_instruction_hint=`Apskatīt <a class="show-instruction">komandrindas izmantošanas norādes</a>.` +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ā <strong>%[1]s</strong> %[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ā <a href="%s">%s</a> -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 <strong>NEATGRIEZENISKA</strong>. 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 <strong>NEATGRIEZENISKA</strong>. Vai turpināt? +settings.delete_desc=Repozitorija dzēšana ir neatgriezeniska un nav atsaucama. settings.delete_notices_1=- Šī darbība ir <strong>NEATGRIEZENISKA</strong>. settings.delete_notices_2=- Šī darbība neatgriezeniski izdzēsīs visu repozitorijā <strong>%s</strong>, 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 <a href="https://github.com/gobwas/glob">dokumentācijā</a>. 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 <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> dokumentācijā. Piemēram, <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. @@ -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 <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">aizsargāto tagu šablonu dokumentācijā</a>. 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 <strong>rakstīšanas</strong> ties teams.admin_permission_desc=Šai komandai ir <strong>administratora</strong> 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 <strong>Veidot repozitorijus</strong> 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 <strong>skatīšanās</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties un klonēt visus organizācijas repozitorijus. teams.all_repositories_write_permission_desc=Šī komanda piešķirt <strong>labošanas</strong> tiesības <strong>visiem repozitorijiem</strong>: komandas biedri var skatīties un nosūtīt izmaiņas visiem organizācijas repozitorijiem. teams.all_repositories_admin_permission_desc=Šī komanda piešķirt <strong>administratora</strong> tiesības <strong>visiem repozitorijiem</strong>: 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 <strong>%[2]s</strong> komandai <strong>%[1]s</strong>. 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 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">mājas lapā</a>. 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 <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">tīmekļa āķu dokumentācijā</a>. 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 <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">tīmekļa āķu dokumentācijā</a>. 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/<jūsu lietotājvārds>/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 (<serveris>/.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 <a href="%s">%s</a> rename_repo=pārsauca repozitoriju no <code>%[1]s</code> uz <a href="%[2]s">%[3]s</a> @@ -3062,7 +3226,7 @@ publish_release=`izveidoja versiju <a href="%[2]s"> "%[4]s" </a> repozitorijā < review_dismissed=`noraidīja lietotāja <b>%[4]s</b> recenziju izmaiņu pieprasījumam <a href="%[1]s">%[3]s#%[2]s</a>` review_dismissed_reason=Iemesls: create_branch=izveidoja atzaru <a href="%[2]s">%[3]s</a> repozitorijā <a href="%[1]s">%[4]s</a> -starred_repo=atzīmēja ar zvaigznīti <a href="%[1]s">%[2]s</a> +starred_repo=pievienoja izlasē <a href="%[1]s">%[2]s</a> watched_repo=sāka sekot <a href="%[1]s">%[2]s</a> [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 <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>. empty.repo=Neparādās augšupielādēta pakotne? Apmeklējiet <a href="%[1]s">pakotņu iestatījumus</a>, lai sasaistītu ar repozitoriju. +registry.documentation=Vairāk informācija par %s reģistru ir pieejama <a target="_blank" rel="noopener noreferrer" href="%s">dokumentācijā</a>. 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 <b>%s</b>. 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 <b>%s</b>. 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: <a href="%[1]s">%[2]s#%[3]d</a>` @@ -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 <a href="%s">%s</a> -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 <strong>Schrijf</strong> rechten: led teams.admin_permission_desc=Dit team heeft <strong>beheersrechten</strong>: leden kunnen van en naar teamrepositories pullen, pushen, en er medewerkers aan toevoegen. teams.create_repo_permission_desc=Daarnaast verleent dit team <strong>Maak repository</strong> 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 (<server>/.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 <a href="%s">%s</a> rename_repo=hernoemde repository van <code>%[1]s</code> naar <a href="%[2]s">%[3]s</a> 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 <b>%s</b>. 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 <b>%s</b>. 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 <a href="%s">%s</a> -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 <strong>z zapisem</str teams.admin_permission_desc=Ten zespół udziela dostępu <strong>administratora</strong>: 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 <strong>Tworzenie repozytoriów</strong>: 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 (<server>/.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 <a href="%s">%s</a> rename_repo=zmienia nazwę repozytorium <code>%[1]s</code> na <a href="%[2]s">%[3]s</a> 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 <strong>não existe</strong> ou <strong>você não está autorizado</strong> 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, <a href="%s">mostrar resultados não filtrados</a>. @@ -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 <b>%s</b>. 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 <b>%s</b>. 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 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">lista de senhas roubadas</a> 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 <strong>habilitada</strong> 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? <a href="%s">Migre o repositório.</a> 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 <code>escopo/item</code> para torná-lo mutuamente exclusivo com outros rótulos do <code>escopo/</code>. 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 <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at=`reabriu este pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` +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 <a href="%s">%s</a> -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 <strong>escrita</str teams.admin_permission_desc=Esta equipe concede acesso de <strong>Administrador</strong>: 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 <strong>Criar repositório</strong>: 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 <strong>Leitura</strong> a <strong>todos os repositórios</strong>: membros podem ver e clonar repositórios. teams.all_repositories_write_permission_desc=Esta equipe concede acesso <strong>Escrita</strong> a <strong>todos os repositórios</strong>: os membros podem ler de e fazer push para os repositórios. teams.all_repositories_admin_permission_desc=Esta equipe concede acesso <strong>Administrativo</strong> a <strong>todos os repositórios</strong>: os membros podem ler, fazer push e adicionar colaboradores aos repositórios. +teams.invite.title=Você foi convidado para fazer parte da equipe <strong>%s</strong> na organização <strong>%s</strong>. 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 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">o blog</a> 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 (<servidor>/.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 <a href="%s">%s</a> rename_repo=renomeou o repositório <code>%[1]s</code> para <a href="%[2]s">%[3]s</a> @@ -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 <strong>não existe</strong> ou <strong>não tem autorização</strong> 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" <email@exemplo.com>. 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. <a href="%s">Mostrar resultados não filtrados</a>. @@ -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 <b>%s</b>. 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 <b>%s</b>. 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 <b>%s</b>. 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 (<b>%s</b>). 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 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">lista de senhas roubadas</a> 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 <b>%s</b> 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: <a target="_blank" rel="noopener noreferrer" href="%s">Escolher uma licença.</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. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver as modificações ou clique em <strong>Cometer novamente</strong> 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 <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`reabriu esta questão <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`referenciou esta questão num cometimento <a id="%[1]s" href="#%[1]s">%[2]s</a>` @@ -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: <a href="%[1]s">%[2]s#%[3]d</a>` 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 <a href="%s">%s</a> -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 <a target="_blank" rel="noreferrer" href="%s">documentação</a> 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 <strong>escrita</stron teams.admin_permission_desc=Esta equipa atribui o acesso de <strong>administração</strong>: 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 <strong>criar repositórios</strong>: 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 <strong>não irá</strong> remover automaticamente os repositórios já adicionados com <i>Todos os repositórios</i>. 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 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a> 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 (<server>/.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 <a href="%s">%s</a> rename_repo=renomeou o repositório de <code>%[1]s</code> para <a href="%[2]s">%[3]s</a> @@ -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 <code>Gemfile</code>: 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 <a target="_blank" rel="noopener noreferrer" href="%s">guia de iniciação rápida</a>. -runs.no_workflows.documentation=Para mais informação sobre o Gitea Action, veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>. +runs.no_workflows.quick_start=Não sabe como começar com o Gitea Actions? Veja o <a target="_blank" rel="noopener noreferrer" href="%s">guia de inicio rápido</a>. +runs.no_workflows.documentation=Para mais informação sobre o Gitea Actions veja <a target="_blank" rel="noopener noreferrer" href="%s">a documentação</a>. 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=Показаны только релевантные репозитории, <a href="%s">показать результаты без фильтрации</a>. @@ -364,7 +360,6 @@ forgot_password_title=Восстановить пароль forgot_password=Забыли пароль? sign_up_now=Нужен аккаунт? Зарегистрируйтесь. sign_up_successful=Учётная запись успешно создана. Добро пожаловать! -confirmation_mail_sent_prompt=Новое письмо для подтверждения направлено на <b>%s</b>. Пожалуйста, проверьте ваш почтовый ящик в течение %s для завершения регистрации. must_change_password=Обновить пароль allow_password_change=Требовать смену пароля пользователем (рекомендуется) reset_password_mail_sent_prompt=Письмо с подтверждением отправлено на <b>%s</b>. Пожалуйста, проверьте входящую почту в течение %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" в <a href="%s">%s</a> -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=Эта команда даёт <strong>административный</strong> доступ: участники могут читать, отправлять изменения и добавлять соавторов к её репозиториям. teams.create_repo_permission_desc=Кроме того, эта команда предоставляет право <strong>Создание репозитория</strong>: члены команды могут создавать новые репозитории в организации. 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. Более подробную информацию читайте в <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">блоге</a>. 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 (<server>/.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=создал(а) репозиторий <a href="%s"> %s</a> rename_repo=переименовал(а) репозиторий из <code>%[1]s</code> на <a href="%[2]s">%[3]s</a> @@ -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? Читайте <a target="_blank" rel="noopener noreferrer" href="%s">руководство по быстрому старту</a>. -runs.no_workflows.documentation=Чтобы узнать больше о Действиях Gitea, читайте <a target="_blank" rel="noopener noreferrer" href="%s">документацию</a>. 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=නව තහවුරු කිරීමේ විද්යුත් තැපෑලක් <b>%s</b>වෙත යවා ඇත. ලියාපදිංචි කිරීමේ ක්රියාවලිය සම්පූර්ණ කිරීම සඳහා කරුණාකර ඊළඟ %s තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න. must_change_password=මුරපදය යාවත්කාල කරන්න allow_password_change=මුරපදය වෙනස් කිරීමට පරිශීලකයාට අවශ්ය වේ (නිර්දේශිත) reset_password_mail_sent_prompt=තහවුරු කිරීමේ විද්යුත් තැපෑලක් <b>%s</b>වෙත යවා ඇත. ඊළඟ තුළ ඔබගේ එන ලිපි පරීක්ෂා කරන්න %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=`මෙම ශාඛා අතර අදින්න ඉල්ලීම දැනටමත් පවතී: <a href="%[1]s">%[2]s #%[3]d</a>` @@ -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=<a href="%s">%s</a> හි "%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=මෙම කණ්ඩායම <strong>පරිපාලක</strong> ප්රවේශය ලබා දෙයි: සාමාජිකයින්ට කියවීමට, කණ්ඩායම් ගබඩාවන්ට සහයෝගීකයින් වෙත තල්ලු කිරීමට සහ එකතු කිරීමට හැකිය. teams.create_repo_permission_desc=මීට අමතරව, මෙම කණ්ඩායම <strong>ලබා දෙයි ගබඩාව සාදන්න</strong> අවසරය: සාමාජිකයින්ට සංවිධානයේ නව ගබඩාවක් නිර්මාණය කළ හැකිය. 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 (<server>/.හොඳින් දැන /openid-වින්යාසය) භාවිතා කරන්න auths.tip.twitter=https://dev.twitter.com/apps වෙත යන්න, යෙදුමක් සාදන්න සහ “මෙම යෙදුම ට්විටර් සමඟ පුරනය වීමට භාවිතා කිරීමට ඉඩ දෙන්න” විකල්පය සක්රීය කර ඇති බවට සහතික වන්න @@ -2456,6 +2442,7 @@ notices.desc=සවිස්තරය notices.op=ඔප්. notices.delete_success=පද්ධති දැන්වීම් මකා දමා ඇත. + [action] create_repo=නිර්මිත ගබඩාව <a href="%s">%s</a> rename_repo=<code>%[1]s</code> සිට <a href="%[2]s">%[3]s</a>දක්වා නම් කරන ලද ගබඩාව 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, <a href="%s">zobraziť nefiltrované výsledky</a>. @@ -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 <b>%s</b> 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 <b>%s</b> 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 <a target="_blank" rel="noopener noreferrer" href="%s">Pomocníka</a>. 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 <b>%s</b>. 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 <b>%s</b>. 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 <code>s[2]s</code> in i <code id="branch_target">%[3]s</code> @@ -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 <a href="%s"> %s</a> -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 <strong>skrivrättighete teams.admin_permission_desc=Medlemskap i detta team ger <strong>administratörsrättigheter</strong>: medlemmar kan läsa, pusha och lägga till medarbetare till teamets utvecklingskataloger. teams.create_repo_permission_desc=Vidare så ger detta team <strong>Skapa utvecklingskatalog</strong> 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 (<server>/.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 <a href="%s"> %s</a> rename_repo=döpte om utvecklingskalatogen från <code>%[1]s</code> till <a href="%[2]s">%[3]s</a> 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 <strong>mevcut değil</strong> veya <strong>görüntüleme yetkiniz yok</strong>. +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 <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">ikili dosyayı çalıştırın</a>, <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a> ile yükleyin veya <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">paket</a> olarak edinin. platform=Farklı platformlarda çalışablir platform_desc=Gitea <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> 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, <a href="%s">süzülmemiş sonuçları göster</a>. @@ -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ı <b>%s</b> 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=<b>%s</b> 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 <a href="%s">buraya</a> 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 <strong>kaydedilmiş</strong>. 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? <a href="%s">Depoyu taşıyın.</a> 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=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için <a href="%s">buraya tıklayın</a>. +blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> 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=`<a href="%[3]s">bu konuyu kapat%[4]s</a> <a id="%[1]s" h issues.ref_reopened_from=`<a href="%[3]s">konuyu yeniden aç%[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` 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=<code>Kapsam/öğe</code> etiketini, diğer <code>kapsam/</code> 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.<br>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=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini kapattı` pulls.reopened_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini yeniden açtı` +pulls.cmd_instruction_hint=`<a class="show-instruction">Komut satırı talimatlarını</a> 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 <a href="%s">%s</a> 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 <a href="https://github.com/gobwas/glob">belgelere</a> 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 <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> belgelerine bakın. Örnekler: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. @@ -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 <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">korumalı etiketler rehberini</a> 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 <strong>Yazma</strong> erişimi veriyor. teams.admin_permission_desc=Bu takım <strong>Yönetici</strong> erişimi veriyor. Üyeler takım depolarını okuyabilir, itebilir ve katkıcı ekleyebilir. teams.create_repo_permission_desc=Ayrıca, bu takım <strong>Depo oluşturma</strong> 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 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blog</a>'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. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> 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. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> 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 (<server>/.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 <a href="%s">%s</a> oluşturuldu rename_repo=<code>%[1]s</code> olan depo adını <a href="%[2]s">%[3]s</a> 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=Новий лист для підтвердження було відправлено на <b>%s</b>, будь ласка, перевірте вашу поштову скриньку протягом %s для завершення реєстрації. must_change_password=Оновіть свій пароль allow_password_change=Вимагати в користувача змінити пароль (рекомендується) reset_password_mail_sent_prompt=Електронний лист із підтвердженням надіслано <b>%s</b>. Перевірте папку 'Вхідні' в межах наступних %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=`Запит злиття для цих гілок вже існує: <a href="%[1]s">%[2]s#%[3]d</a>` @@ -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" в <a href="%s">%s</a> -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=Ця команда надає доступ на <st teams.admin_permission_desc=Ця команда надає <strong>адміністраторський</strong> доступ: учасники можуть читати, виконувати push команди та додавати співробітників до репозиторію. teams.create_repo_permission_desc=Крім того, ця команда надає дозвіл <strong>Створити репозиторій</strong>: учасники можуть створювати нові репозиторії в організації. 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 (<server>/.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=створив(ла) репозиторій <a href="%s">%s</a> rename_repo=репозиторій перейменовано з <code>%[1]s</code> на <a href="%[2]s">%[3]s</a> 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=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。 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=只显示相关的仓库, <a href="%s">显示未过滤结果</a>。 @@ -366,7 +389,6 @@ forgot_password_title=忘记密码 forgot_password=忘记密码? sign_up_now=还没帐户?马上注册。 sign_up_successful=帐户创建成功。欢迎! -confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %s 内完成确认注册操作。 must_change_password=更新您的密码 allow_password_change=要求用户更改密码(推荐) reset_password_mail_sent_prompt=确认电子邮件已被发送到 <b>%s</b>。请您在 %s 内检查您的收件箱 ,完成密码重置过程。 @@ -423,6 +445,7 @@ authorization_failed_desc=因为检测到无效请求,授权失败。请尝试 sspi_auth_failed=SSPI 认证失败 password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。 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=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合你的项目?见 <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许可证</a> +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=这些分支之间的合并请求已存在: <a href="%[1]s">%[2]s#%[3]d</a> 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=在 <a href="%[2]s"> %[3]s </a> 中搜索 "%[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 钩子似乎已损坏。 请按照 <a target="_blank" rel="noreferrer" href="%s">文档</a> 来修复它们,然后推送一些提交来刷新状态。 + +[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=该团队拥有对所属仓库的 <strong>读取</st teams.admin_permission_desc=该团队拥有一定的 <strong>管理</strong> 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。 teams.create_repo_permission_desc=此外,该团队拥有了 <strong>创建仓库</strong> 的权限:成员可以在组织中创建新的仓库。 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=团队成员将只能访问添加到团队的仓库。 选择此项 <strong>将不会</strong> 自动删除已经添加的仓库。 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。查看 <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">博客</a> 了解详情。 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 (<server>/.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=创建了仓库 <a href="%s">%s</a> rename_repo=重命名仓库 <code>%[1]s</code> 为 <a href="%[2]s">%[3]s</a> @@ -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?请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">快速启动指南</a> -runs.no_workflows.documentation=更多有关 Gitea Action 的信息,请访问 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。 +runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗?请查看 <a target="_blank" rel="noopener noreferrer" href="%s">快速启动指南</a>。 +runs.no_workflows.documentation=关于Gitea Actions的更多信息,请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。 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=一封新的確認郵件已發送至 <b>%s</b>。請檢查您的收件箱並在 %s 小時內完成確認註冊操作。 active_your_account=啟用您的帳戶 has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到啟用郵件,或需要重新發送,請單擊下方的按鈕。 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 次代碼提交從 <code>%[2]s</code>合併至 <code>%[3]s</code> 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 (<server>/.well-known/openid-configuration) 來指定節點 auths.delete=刪除認證來源 auths.delete_auth_title=刪除認證來源 @@ -915,6 +919,7 @@ notices.desc=描述 notices.op=操作 notices.delete_success=已刪除系統提示。 + [action] create_repo=建立了儲存庫 <a href="%s">%s</a> rename_repo=重新命名儲存庫 <code>%[1]s</code> 為 <a href="%[2]s">%[3]s</a> 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=只顯示相關的儲存庫,<a href="%s">顯示未篩選的結果</a>。 @@ -341,7 +337,6 @@ remember_me=記得這個裝置 forgot_password_title=忘記密碼 forgot_password=忘記密碼? sign_up_now=還沒有帳戶?馬上註冊。 -confirmation_mail_sent_prompt=新的確認信已發送至 <b>%s</b>。請在 %s內檢查您的收件匣並完成註冊作業。 must_change_password=更新您的密碼 allow_password_change=要求使用者更改密碼 (推薦) reset_password_mail_sent_prompt=確認信已發送至 <b>%s</b>。請在 %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=`已有介於這些分支間的合併請求:<a href="%[1]s">%[2]s#%[3]d</a>` @@ -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=在 <a href="%s"> %s </a> 中搜尋 "%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=這個團隊擁有<strong>寫入</strong> 權限: teams.admin_permission_desc=這個團隊擁有<strong>管理員</strong> 權限:成員可以讀取、推送和增加協作者到儲存庫。 teams.create_repo_permission_desc=此外,這個團隊還擁有<strong>建立儲存庫</strong>的權限:成員可以在組織中新增儲存庫。 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。詳情請參閱<a target="_blank" rel="noreferrer" href="https://blog.gitea.io">部落格</a>的說明。 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 (<server>/.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=建立了儲存庫 <a href="%s">%s</a> rename_repo=重新命名儲存庫 <code>%[1]s</code> 為 <a href="%[2]s">%[3]s</a> @@ -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 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32"><path style="fill:#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"/><path style="fill:#609926" d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path style="fill:#609926" d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640" xml:space="preserve" width="32" height="32"><path style="fill:#fff" d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"/><path style="fill:#609926" d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"/><path style="fill:#609926" d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-bitbucket__svg gitea-bitbucket__gitea-bitbucket svg gitea-bitbucket" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 295" width="16" height="16"><g fill="#205081"><path d="M128 0C57.732 0 .012 18.822.012 42.663c0 6.274 15.057 95.364 21.331 130.498 2.51 16.312 43.918 38.898 106.657 38.898 62.74 0 102.893-22.586 106.657-38.898 6.274-35.134 21.331-124.224 21.331-130.498C254.734 18.822 198.268 0 128 0m0 183.199c-22.586 0-40.153-17.567-40.153-40.153s17.567-40.153 40.153-40.153 40.153 17.567 40.153 40.153c0 21.331-17.567 40.153-40.153 40.153m0-127.988c-45.172 0-81.561-7.53-81.561-17.567 0-10.039 36.389-17.567 81.561-17.567s81.561 7.528 81.561 17.567c0 10.038-36.389 17.567-81.561 17.567"/><path d="M220.608 207.04c-2.51 0-3.764 1.255-3.764 1.255s-31.37 25.096-87.835 25.096c-56.466 0-87.835-25.096-87.835-25.096s-2.51-1.255-3.765-1.255c-2.51 0-5.019 1.255-5.019 5.02v1.254c5.02 26.35 8.784 45.172 8.784 47.682 3.764 18.822 41.408 33.88 86.58 33.88s82.816-15.058 86.58-33.88c0-2.51 3.765-21.332 8.784-47.682v-1.255c1.255-2.51 0-5.019-2.51-5.019"/><circle cx="128" cy="141.791" r="20.077"/></g></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42" class="svg gitea-bitbucket" width="16" height="16" aria-hidden="true"><defs><linearGradient id="gitea-bitbucket__a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#gitea-bitbucket__a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.7 2.7 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-discord__svg gitea-discord__gitea-discord svg gitea-discord" preserveAspectRatio="xMidYMid" viewBox="0 0 256 293" width="16" height="16"><path fill="#7289DA" d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0m-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732a2 2 0 0 1-.585-.438c-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.8 123.8 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238s-6.582-16.237-14.92-16.237"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" class="svg gitea-discord" width="16" height="16" aria-hidden="true"><path fill="#5865f2" d="M107.7 8.07A105.2 105.2 0 0 0 81.47 0a72 72 0 0 0-3.36 6.83 97.7 97.7 0 0 0-29.11 0A72 72 0 0 0 45.64 0a106 106 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.7 105.7 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.4 68.4 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.7 68.7 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.3 105.3 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15M42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69m42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69"/></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-facebook__svg gitea-facebook__gitea-facebook svg gitea-facebook" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" width="16" height="16"><path fill="#395b97" d="M93.5 8.5q-2.177 1.203-5 1.5L10 88.5q-.297 2.823-1.5 5a552 552 0 0 1-.5-56Q11.75 11.75 37.5 8a552 552 0 0 1 56 .5" style="opacity:.995"/><path fill="#366098" d="M93.5 8.5q23.832 6.337 26 31a677 677 0 0 0-1.5 37l-35 35a32.4 32.4 0 0 0-.5 8 442 442 0 0 1-1-42h14a380 380 0 0 0 3-17h-17q-3.75-20.745 17-18v-16q-38.632-4.865-33 34h-14v17h14v42q-14.01.25-28-.5-23.177-2.93-29-25.5 1.203-2.177 1.5-5L88.5 10q2.823-.297 5-1.5" style="opacity:.976"/><path fill="#346499" d="M119.5 39.5q.25 25.005-.5 50-5.432 30.368-36.5 30a32.4 32.4 0 0 1 .5-8l35-35q.254-18.76 1.5-37" style="opacity:.918"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222" class="svg gitea-facebook" width="16" height="16" aria-hidden="true"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167z"/></g></svg> \ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 70 70" class="svg gitea-jetbrains" width="16" height="16" aria-hidden="true"><linearGradient id="gitea-jetbrains__a" x1=".79" x2="33.317" y1="40.089" y2="40.089" gradientUnits="userSpaceOnUse"><stop offset=".258" style="stop-color:#f97a12"/><stop offset=".459" style="stop-color:#b07b58"/><stop offset=".724" style="stop-color:#577bae"/><stop offset=".91" style="stop-color:#1e7ce5"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M17.7 54.6.8 41.2l8.4-15.6L33.3 35z" style="fill:url(#gitea-jetbrains__a)"/><linearGradient id="gitea-jetbrains__b" x1="25.767" x2="79.424" y1="24.88" y2="54.57" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#f97a12"/><stop offset=".072" style="stop-color:#cb7a3e"/><stop offset=".154" style="stop-color:#9e7b6a"/><stop offset=".242" style="stop-color:#757b91"/><stop offset=".334" style="stop-color:#537bb1"/><stop offset=".432" style="stop-color:#387ccc"/><stop offset=".538" style="stop-color:#237ce0"/><stop offset=".655" style="stop-color:#147cef"/><stop offset=".792" style="stop-color:#0b7cf7"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="m70 18.7-1.3 40.5L41.8 70 25.6 59.6 49.3 35 38.9 12.3l9.3-11.2z" style="fill:url(#gitea-jetbrains__b)"/><linearGradient id="gitea-jetbrains__c" x1="63.228" x2="48.29" y1="42.915" y2="-1.719" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".078" style="stop-color:#cb417e"/><stop offset=".16" style="stop-color:#9e4e9b"/><stop offset=".247" style="stop-color:#755bb4"/><stop offset=".339" style="stop-color:#5365ca"/><stop offset=".436" style="stop-color:#386ddb"/><stop offset=".541" style="stop-color:#2374e9"/><stop offset=".658" style="stop-color:#1478f3"/><stop offset=".794" style="stop-color:#0b7bf8"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M70 18.7 48.7 43.9l-9.8-31.6 9.3-11.2z" style="fill:url(#gitea-jetbrains__c)"/><linearGradient id="gitea-jetbrains__d" x1="10.72" x2="55.524" y1="16.473" y2="90.58" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#fe315d"/><stop offset=".04" style="stop-color:#f63462"/><stop offset=".104" style="stop-color:#df3a71"/><stop offset=".167" style="stop-color:#c24383"/><stop offset=".291" style="stop-color:#ad4a91"/><stop offset=".55" style="stop-color:#755bb4"/><stop offset=".917" style="stop-color:#1d76ed"/><stop offset="1" style="stop-color:#087cfa"/></linearGradient><path d="M33.7 58.1 5.6 68.3l4.5-15.8L16 33.1 0 27.7 10.1 0l22 2.7 21.6 24.7z" style="fill:url(#gitea-jetbrains__d)"/><path d="M13.7 13.5h43.2v43.2H13.7z" style="fill:#000"/><path d="M17.7 48.6h16.2v2.7H17.7zM29.4 22.4v-3.3h-9v3.3H23v11.3h-2.6V37h9v-3.3h-2.5V22.4zM38 37.3c-1.4 0-2.6-.3-3.5-.8s-1.7-1.2-2.3-1.9l2.5-2.8c.5.6 1 1 1.5 1.3s1.1.5 1.7.5c.7 0 1.3-.2 1.8-.7.4-.5.6-1.2.6-2.3V19.1h4v11.7c0 1.1-.1 2-.4 2.8s-.7 1.4-1.3 2c-.5.5-1.2 1-2 1.2-.8.3-1.6.5-2.6.5" style="fill:#fff"/></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="gitea-microsoftonline__svg gitea-microsoftonline__gitea-microsoftonline svg gitea-microsoftonline" viewBox="0 0 2075 2499.8" width="16" height="16"><path fill="#eb3c00" d="M0 2016.6V496.8L1344.4 0 2075 233.7v2045.9l-730.6 220.3zl1344.4 161.8V409.2L467.6 613.8v1198.3z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48" class="svg gitea-microsoftonline" width="16" height="16" aria-hidden="true"><path fill="url(#gitea-microsoftonline__a)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__b)" d="m20.084 3.026-.224.136a8 8 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258q.111-.068.224-.131Z"/><path fill="url(#gitea-microsoftonline__c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26z"/><path fill="url(#gitea-microsoftonline__e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31q.004-.132.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#gitea-microsoftonline__g)" d="M4.004 30.998"/><path fill="url(#gitea-microsoftonline__h)" d="M4.004 30.998"/><defs><radialGradient id="gitea-microsoftonline__a" cx="0" cy="0" r="1" gradientTransform="rotate(110.528 5.021 11.358)scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="gitea-microsoftonline__c" cx="0" cy="0" r="1" gradientTransform="matrix(30.7198 -4.51832 2.98465 20.29248 10.43 36.351)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="gitea-microsoftonline__e" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="gitea-microsoftonline__g" cx="0" cy="0" r="1" gradientTransform="matrix(-24.1583 -6.12555 10.3118 -40.66824 41.055 26.504)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="gitea-microsoftonline__b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="gitea-microsoftonline__h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg> \ 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 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" aria-hidden="true" class="gitea-twitter__svg gitea-twitter__gitea-twitter svg gitea-twitter" clip-rule="evenodd" viewBox="-89.009 -46.884 643.937 446.884" width="16" height="16"><path fill="#1da1f2" fill-rule="nonzero" d="M154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052A205.3 205.3 0 0 0 492 47.346c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.4 202.4 0 0 1-64.098 24.511C395.903 12.276 369.679 0 340.641 0c-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504A100.74 100.74 0 0 0 20.57 69.24c0 35.034 17.82 65.961 44.92 84.055a100.2 100.2 0 0 1-45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101 101 0 0 1-26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224A205 205 0 0 1 0 354.634c44.674 28.645 97.72 45.359 154.734 45.359"/></svg> \ No newline at end of file +<svg viewBox="0 0 24 24" class="svg gitea-twitter" xmlns="http://www.w3.org/2000/svg" width="16" height="16" aria-hidden="true"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982z"/></svg> \ 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 16 16" class="svg gitea-vscodium" width="16" height="16" aria-hidden="true"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9l.8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3s0 2.5-.2 3.7c0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8s.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg> \ 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 `<p>Wiki! Enjoy :)</p> <ul> -<li><a href="` + AppSubURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li> -<li><a href="` + AppSubURL + `wiki/Tips" rel="nofollow">Tips</a></li> +<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li> +<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li> <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li> </ul> `, // Guard wiki sidebar: special syntax `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, // rendered - `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> + `<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> `, // special syntax `[[Name|Link]]`, // rendered - `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p> + `<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p> `, // empty ``, @@ -111,8 +111,8 @@ Here are some links to the most important topics. You can find the full list of <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p> <h2 id="user-content-quick-links">Quick Links</h2> <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p> -<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a> -<a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> +<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a> +<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> `, } 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", "<br>") } - -// 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(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)) + ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name))) } else { - ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)) + ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName))) } - ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>" + ctx.Data["ApplicationRedirectDomainHTML"] = template.HTML("<strong>" + html.EscapeString(form.RedirectURI) + "</strong>") // 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 = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>" } 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) + "<br>" + 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) + "<br>" + 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) + "<br>" + 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) + "<br>" + 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(), `<div id="code-comments-`) }) - run("timeline without outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { + run("timeline is not affected by ShowOutdatedComments=false", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { ctx.Data["ShowOutdatedComments"] = false renderConversation(ctx, preparedComment, "timeline") - assert.Contains(t, resp.Body.String(), `conversation-not-existing`) + assert.Contains(t, resp.Body.String(), `<div id="code-comments-`) + }) + run("diff non-existing review", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { + err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{}) + assert.NoError(t, err) + ctx.Data["ShowOutdatedComments"] = true + renderConversation(ctx, preparedComment, "diff") + assert.Equal(t, http.StatusOK, resp.Code) + assert.NotContains(t, resp.Body.String(), `status-page-500`) + }) + run("timeline non-existing review", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) { + err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{}) + assert.NoError(t, err) + ctx.Data["ShowOutdatedComments"] = true + renderConversation(ctx, preparedComment, "timeline") + assert.Equal(t, http.StatusOK, resp.Code) + assert.NotContains(t, resp.Body.String(), `status-page-500`) }) } diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go new file mode 100644 index 0000000000..c158fb30b6 --- /dev/null +++ b/routers/web/repo/recent_commits.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 ( + tplRecentCommits base.TplName = "repo/activity" +) + +// RecentCommits renders the page to show recent commit frequency on repository +func RecentCommits(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.recent_commits") + + ctx.Data["PageIsActivity"] = true + ctx.Data["PageIsRecentCommits"] = true + ctx.PageData["repoLink"] = ctx.Repo.RepoLink + + ctx.HTML(http.StatusOK, tplRecentCommits) +} + +// RecentCommitsData returns JSON of recent commits data +func RecentCommitsData(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("RecentCommitsData", err) + } else { + ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) + } +} diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index fdb247d413..7ba23f0701 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -12,20 +12,22 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" 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/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/upload" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/feed" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" releaseservice "code.gitea.io/gitea/services/release" ) @@ -67,6 +69,88 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model return nil } +type ReleaseInfo struct { + Release *repo_model.Release + CommitStatus *git_model.CommitStatus + CommitStatuses []*git_model.CommitStatus +} + +func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) { + releases, err := db.Find[repo_model.Release](ctx, opts) + if err != nil { + return nil, err + } + + for _, release := range releases { + release.Repo = ctx.Repo.Repository + } + + if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil { + return nil, err + } + + // Temporary cache commits count of used branches to speed up. + countCache := make(map[string]int64) + cacheUsers := make(map[int64]*user_model.User) + if ctx.Doer != nil { + cacheUsers[ctx.Doer.ID] = ctx.Doer + } + var ok bool + + canReadActions := ctx.Repo.CanRead(unit.TypeActions) + + releaseInfos := make([]*ReleaseInfo, 0, len(releases)) + for _, r := range releases { + if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { + r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + r.Publisher = user_model.NewGhostUser() + } else { + return nil, err + } + } + cacheUsers[r.PublisherID] = r.Publisher + } + + r.RenderedNote, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.Repo.RepoLink, + }, + Metas: ctx.Repo.Repository.ComposeMetas(ctx), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, r.Note) + if err != nil { + return nil, err + } + + if !r.IsDraft { + if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil { + return nil, err + } + } + + info := &ReleaseInfo{ + Release: r, + } + + if canReadActions { + statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll) + if err != nil { + return nil, err + } + + info.CommitStatus = git_model.CalcCommitStatus(statuses) + info.CommitStatuses = statuses + } + + releaseInfos = append(releaseInfos, info) + } + + return releaseInfos, nil +} + // Releases render releases list page func Releases(ctx *context.Context) { ctx.Data["PageIsReleaseList"] = true @@ -91,77 +175,26 @@ func Releases(ctx *context.Context) { writeAccess := ctx.Repo.CanWrite(unit.TypeReleases) ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived - opts := repo_model.FindReleasesOptions{ + releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{ ListOptions: listOptions, // only show draft releases for users who can write, read-only users shouldn't see draft releases. IncludeDrafts: writeAccess, RepoID: ctx.Repo.Repository.ID, - } - - releases, err := db.Find[repo_model.Release](ctx, opts) + }) if err != nil { - ctx.ServerError("GetReleasesByRepoID", err) + ctx.ServerError("getReleaseInfos", err) return } - - for _, release := range releases { - release.Repo = ctx.Repo.Repository - } - - if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil { - ctx.ServerError("GetReleaseAttachments", err) - return - } - - // Temporary cache commits count of used branches to speed up. - countCache := make(map[string]int64) - cacheUsers := make(map[int64]*user_model.User) - if ctx.Doer != nil { - cacheUsers[ctx.Doer.ID] = ctx.Doer - } - var ok bool - - for _, r := range releases { - if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { - r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - r.Publisher = user_model.NewGhostUser() - } else { - ctx.ServerError("GetUserByID", err) - return - } - } - cacheUsers[r.PublisherID] = r.Publisher - } - - r.Note, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.Repo.RepoLink, - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, r.Note) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - - if r.IsDraft { - continue - } - - if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil { - ctx.ServerError("calReleaseNumCommitsBehind", err) - return + for _, rel := range releases { + if rel.Release.IsTag && rel.Release.Title == "" { + rel.Release.Title = rel.Release.TagName } } ctx.Data["Releases"] = releases numReleases := ctx.Data["NumReleases"].(int64) - pager := context.NewPagination(int(numReleases), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager @@ -196,7 +229,7 @@ func TagsList(ctx *context.Context) { // the drafts should also be included because a real tag might be used as a draft. IncludeDrafts: true, IncludeTags: true, - HasSha1: util.OptionalBoolTrue, + HasSha1: optional.Some(true), RepoID: ctx.Repo.Repository.ID, } @@ -249,15 +282,28 @@ func SingleRelease(ctx *context.Context) { writeAccess := ctx.Repo.CanWrite(unit.TypeReleases) ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived - release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*")) + releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{ + ListOptions: db.ListOptions{Page: 1, PageSize: 1}, + RepoID: ctx.Repo.Repository.ID, + TagNames: []string{ctx.Params("*")}, + // only show draft releases for users who can write, read-only users shouldn't see draft releases. + IncludeDrafts: writeAccess, + IncludeTags: true, + }) if err != nil { - if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound("GetRelease", err) - return - } - ctx.ServerError("GetReleasesByRepoID", err) + ctx.ServerError("getReleaseInfos", err) return } + if len(releases) != 1 { + ctx.NotFound("SingleRelease", err) + return + } + + release := releases[0].Release + if release.IsTag && release.Title == "" { + release.Title = release.TagName + } + ctx.Data["PageIsSingleTag"] = release.IsTag if release.IsTag { ctx.Data["Title"] = release.TagName @@ -265,43 +311,7 @@ func SingleRelease(ctx *context.Context) { ctx.Data["Title"] = release.Title } - release.Repo = ctx.Repo.Repository - - err = repo_model.GetReleaseAttachments(ctx, release) - if err != nil { - ctx.ServerError("GetReleaseAttachments", err) - return - } - - release.Publisher, err = user_model.GetUserByID(ctx, release.PublisherID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - release.Publisher = user_model.NewGhostUser() - } else { - ctx.ServerError("GetUserByID", err) - return - } - } - if !release.IsDraft { - if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil { - ctx.ServerError("calReleaseNumCommitsBehind", err) - return - } - } - release.Note, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.Repo.RepoLink, - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, release.Note) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - - ctx.Data["Releases"] = []*repo_model.Release{release} + ctx.Data["Releases"] = releases ctx.HTML(http.StatusOK, tplReleasesList) } diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go index c4a2c1904e..7ebea4c3fb 100644 --- a/routers/web/repo/release_test.go +++ b/routers/web/repo/release_test.go @@ -10,8 +10,8 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/contexttest" "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/repo/render.go b/routers/web/repo/render.go index 7eb5a42aa4..e64db03e20 100644 --- a/routers/web/repo/render.go +++ b/routers/web/repo/render.go @@ -10,11 +10,12 @@ import ( "path" "code.gitea.io/gitea/modules/charset" - "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/typesniffer" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" ) // RenderFile renders a file by repos path @@ -44,20 +45,17 @@ func RenderFile(ctx *context.Context) { isTextFile := st.IsText() rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) + ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts") if markupType := markup.Type(blob.Name()); markupType == "" { if isTextFile { - _, err = io.Copy(ctx.Resp, rd) - if err != nil { - ctx.ServerError("Copy", err) - } - return + _, _ = io.Copy(ctx.Resp, rd) + } else { + http.Error(ctx.Resp, "Unsupported file type render", http.StatusInternalServerError) } - ctx.Error(http.StatusInternalServerError, "Unsupported file type render") return } - ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts") err = markup.Render(&markup.RenderContext{ Ctx: ctx, RelativePath: ctx.Repo.TreePath, @@ -71,7 +69,8 @@ func RenderFile(ctx *context.Context) { InStandalonePage: true, }, rd, ctx.Resp) if err != nil { - ctx.ServerError("Render", err) + log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err) + http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError) return } } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index bede21be17..4e448933c7 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -21,19 +21,21 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/cache" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "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/storage" 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" repo_service "code.gitea.io/gitea/services/repository" archiver_service "code.gitea.io/gitea/services/repository/archiver" + commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" ) const ( @@ -243,7 +245,7 @@ func CreatePost(ctx *context.Context) { var repo *repo_model.Repository var err error if form.RepoTemplate > 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, `<a class=" item navigation" href="/SubPath/?page=2">`) + + setting.AppSubURL = "" + out, err = ctx.RenderToHTML("base/paginate", map[string]any{"Link": setting.AppSubURL, "Page": page}) + assert.NoError(t, err) + assert.Contains(t, out, `<a class=" item navigation" href="/?page=2">`) +} 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 class="gt-object-contain gt-mr-3" width="%d" height="%d" src="%s" alt="%s">`, + img := fmt.Sprintf(`<img class="tw-object-contain tw-mr-2" width="%d" height="%d" src="%s" alt="%s">`, 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(`<input type="hidden" name="_csrf" value="` + ctx.Data["CsrfToken"].(string) + `">`) @@ -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(`<a href="%s/commit/%s">%s</a>`, 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/<ref> 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, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match") assert.Equal(t, "<mailto:"+replyTo+">", 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: <p>, <h?>, 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, `<div class="code-preview-container file-content"> + <div class="code-preview-header"> + <a href="http://full" class="muted" rel="nofollow">/README.md</a> + repo.code_preview_line_from_to:1,2,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a> + </div> + <table class="file-view"> + <tbody><tr> + <td class="lines-num"><span data-line-number="1"></span></td> + <td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td> + </tr><tr> + <td class="lines-num"><span data-line-number="2"></span></td> + <td class="lines-code chroma"><div class="code-inner"></span><span class="gh"></span></div></td> + </tr></tbody> + </table> +</div> +`, 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, `<div class="code-preview-container file-content"> + <div class="code-preview-header"> + <a href="http://full" class="muted" rel="nofollow">/README.md</a> + repo.code_preview_line_in:1,<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a> + </div> + <table class="file-view"> + <tbody><tr> + <td class="lines-num"><span data-line-number="1"></span></td> + <td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td> + </tr></tbody> + </table> +</div> +`, 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 <remote> +refs/tags/*:refs/tags/*) in %v: Stdout: %s\nError: %v", repo, stdout, err) + return repo, fmt.Errorf("error in MigrateRepositoryGitData(git config --add <remote> +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(`<a href="%s">%s</a>`, 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:<br>", 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 += "<br>" @@ -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(`<a [^>]*?href="([^">]*?)">(.*?)</a>`) @@ -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, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>:<a href="http://localhost:3000/test/repo/src/branch/test">test</a>] 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>: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, `[<a href="http://localhost:3000/test/repo">test/repo</a>: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, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, 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, `<a href="http://localhost:3000/test/repo2">test/repo2</a> is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] user1 pushed 2 commits to <a href="http://localhost:3000/test/repo/src/branch/test">test</a>:<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: commit message - user1<br><a href="http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778">2020558</a>: 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue opened: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on issue <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request opened: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New comment on pull request <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Pull request review approved: <a href="http://localhost:3000/test/repo/pulls/12">#12 Fix bug</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer</a>] Package published by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer</a>] Package published by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] branch created by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 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, "[<http://localhost:3000/test/repo|test/repo>:test] branch deleted by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>: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, "<http://localhost:3000/test/repo2|test/repo2> is forked to <http://localhost:3000/test/repo|test/repo>", pl.(*SlackPayload).Text) + assert.Equal(t, "<http://localhost:3000/test/repo2|test/repo2> is forked to <http://localhost:3000/test/repo|test/repo>", 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, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 2 new commits pushed by user1", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 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, "[<http://localhost:3000/test/repo|test/repo>] Issue opened: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue opened: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] Issue closed: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Issue closed: <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] New comment on issue <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on issue <http://localhost:3000/test/repo/issues/2|#2 crash> by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] Pull request opened: <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request opened: <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] New comment on pull request <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New comment on pull request <http://localhost:3000/test/repo/pulls/12|#12 Fix bug> by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository created by <https://try.gitea.io/user1|user1>", 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: <http://localhost:3000/user1/-/packages/container/GiteaContainer/latest|GiteaContainer:latest> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "Package created: <http://localhost:3000/user1/-/packages/container/GiteaContainer/latest|GiteaContainer:latest> by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] New wiki page '<http://localhost:3000/test/repo/wiki/index|index>' (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] New wiki page '<http://localhost:3000/test/repo/wiki/index|index>' (Wiki change comment) by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' edited (Wiki change comment) by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' edited (Wiki change comment) by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' deleted by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Wiki page '<http://localhost:3000/test/repo/wiki/index|index>' deleted by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", pl.(*SlackPayload).Text) + assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Release created: <http://localhost:3000/test/repo/releases/tag/v1.0|v1.0> by <https://try.gitea.io/user1|user1>", 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, "[<http://localhost:3000/test/repo|test/repo>:<http://localhost:3000/test/repo/src/branch/test|test>] 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(`[<a href="%s">%s</a>] %s <a href="%s">%s</a> 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(`[<a href="%s">%s</a>] %s <a href="%s">%s</a> 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 <a href="%s">%s</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> created`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> deleted`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] branch <a href="http://localhost:3000/test/repo/src/test">test</a> 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 <a href="http://localhost:3000/test/repo">test/repo</a>`, pl.(*TelegramPayload).Message) + assert.Equal(t, `test/repo2 is forked to <a href="http://localhost:3000/test/repo">test/repo</a>`, 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, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1", pl.(*TelegramPayload).Message) + assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] 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, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Issue opened: <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\n\nissue body", pl.(*TelegramPayload).Message) + assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Issue opened: <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Issue closed: <a href="http://localhost:3000/test/repo/issues/2">#2 crash</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on issue <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nmore info needed", pl.(*TelegramPayload).Message) + assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on issue <a href=\"http://localhost:3000/test/repo/issues/2\">#2 crash</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\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, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Pull request opened: <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nfixes bug #2", pl.(*TelegramPayload).Message) + assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] Pull request opened: <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\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, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on pull request <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\nchanges requested", pl.(*TelegramPayload).Message) + assert.Equal(t, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] New comment on pull request <a href=\"http://localhost:3000/test/repo/pulls/12\">#12 Fix bug</a> by <a href=\"https://try.gitea.io/user1\">user1</a>\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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Repository created`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] 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: <a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer:latest</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message) + assert.Equal(t, `Package created: <a href="http://localhost:3000/user1/-/packages/container/GiteaContainer/latest">GiteaContainer:latest</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] New wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' edited (Wiki change comment) by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Wiki page '<a href="http://localhost:3000/test/repo/wiki/index">index</a>' deleted by <a href="https://try.gitea.io/user1">user1</a>`, 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, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, pl.(*TelegramPayload).Message) + assert.Equal(t, `[<a href="http://localhost:3000/test/repo">test/repo</a>] Release created: <a href="http://localhost:3000/test/repo/releases/tag/v1.0">v1.0</a> by <a href="https://try.gitea.io/user1">user1</a>`, 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, "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>:<a href=\"http://localhost:3000/test/repo/src/test\">test</a>] 2 new commits\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] commit message - user1\n[<a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>] 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(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\"> %s</font> \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(" ><font color=\"info\">%s</font>\n >%s \n ><font color=\"warning\">%s</font> \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("> <font color=\"info\"> %s </font> \r\n > <font color=\"comment\">%s </font> \r\n > <font color=\"comment\">%s </font> \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 @@ <label for="port">{{ctx.Locale.Tr "admin.auths.port"}}</label> <input id="port" name="port" value="{{$cfg.Port}}" placeholder="636" required> </div> - <div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}"> + <div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}"> <div class="ui checkbox"> <label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label> <input name="skip_verify" type="checkbox" {{if .Source.SkipVerify}}checked{{end}}> @@ -113,7 +113,7 @@ <input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if $cfg.GroupsEnabled}}checked{{end}}> </div> </div> - <div id="ldap-group-options" class="ui segment secondary {{if not $cfg.GroupsEnabled}}gt-hidden{{end}}"> + <div id="ldap-group-options" class="ui segment secondary {{if not $cfg.GroupsEnabled}}tw-hidden{{end}}"> <div class="field"> <label>{{ctx.Locale.Tr "admin.auths.group_search_base"}}</label> <input name="group_dn" value="{{$cfg.GroupDN}}" placeholder="ou=group,dc=mydomain,dc=com"> @@ -148,7 +148,7 @@ <input id="use_paged_search" name="use_paged_search" type="checkbox" {{if $cfg.UsePagedSearch}}checked{{end}}> </div> </div> - <div class="field required search-page-size{{if not $cfg.UsePagedSearch}} gt-hidden{{end}}"> + <div class="field required search-page-size{{if not $cfg.UsePagedSearch}} tw-hidden{{end}}"> <label for="search_page_size">{{ctx.Locale.Tr "admin.auths.search_page_size"}}</label> <input id="search_page_size" name="search_page_size" value="{{if $cfg.UsePagedSearch}}{{$cfg.SearchPageSize}}{{end}}"> </div> @@ -205,7 +205,7 @@ </div> <p class="help">{{ctx.Locale.Tr "admin.auths.force_smtps_helper"}}</p> </div> - <div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}"> + <div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}"> <div class="ui checkbox"> <label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label> <input name="skip_verify" type="checkbox" {{if $cfg.SkipVerify}}checked{{end}}> 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" .}} <!-- PAM --> - <div class="pam required field {{if not (eq .type 4)}}gt-hidden{{end}}"> + <div class="pam required field {{if not (eq .type 4)}}tw-hidden{{end}}"> <label for="pam_service_name">{{ctx.Locale.Tr "admin.auths.pam_service_name"}}</label> <input id="pam_service_name" name="pam_service_name" value="{{.pam_service_name}}"> <label for="pam_email_domain">{{ctx.Locale.Tr "admin.auths.pam_email_domain"}}</label> <input id="pam_email_domain" name="pam_email_domain" value="{{.pam_email_domain}}"> </div> - <div class="pam optional field {{if not (eq .type 4)}}gt-hidden{{end}}"> + <div class="pam optional field {{if not (eq .type 4)}}tw-hidden{{end}}"> <div class="ui checkbox"> <label for="skip_local_two_fa"><strong>{{ctx.Locale.Tr "admin.auths.skip_local_two_fa"}}</strong></label> <input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if .skip_local_two_fa}}checked{{end}}> @@ -59,7 +59,7 @@ <input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}> </div> </div> - <div class="ldap inline field {{if not (eq .type 2)}}gt-hidden{{end}}"> + <div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}"> <div class="ui checkbox"> <label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label> <input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}> @@ -99,7 +99,7 @@ <li>GitHub</li> <span>{{ctx.Locale.Tr "admin.auths.tip.github"}}</span> <li>GitLab</li> - <span>{{ctx.Locale.Tr "admin.auths.tip.gitlab"}}</span> + <span>{{ctx.Locale.Tr "admin.auths.tip.gitlab_new"}}</span> <li>Google</li> <span>{{ctx.Locale.Tr "admin.auths.tip.google_plus"}}</span> <li>OpenID Connect</li> 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 @@ -<div class="ldap dldap field {{if not (or (eq .type 2) (eq .type 5))}}gt-hidden{{end}}"> +<div class="ldap dldap field {{if not (or (eq .type 2) (eq .type 5))}}tw-hidden{{end}}"> <div class="inline required field {{if .Err_SecurityProtocol}}error{{end}}"> <label>{{ctx.Locale.Tr "admin.auths.security_protocol"}}</label> <div class="ui selection security-protocol dropdown"> @@ -20,17 +20,17 @@ <label for="port">{{ctx.Locale.Tr "admin.auths.port"}}</label> <input id="port" name="port" value="{{.port}}" placeholder="636"> </div> - <div class="has-tls inline field {{if not .HasTLS}}gt-hidden{{end}}"> + <div class="has-tls inline field {{if not .HasTLS}}tw-hidden{{end}}"> <div class="ui checkbox"> <label><strong>{{ctx.Locale.Tr "admin.auths.skip_tls_verify"}}</strong></label> <input name="skip_verify" type="checkbox" {{if .skip_verify}}checked{{end}}> </div> </div> - <div class="ldap field {{if not (eq .type 2)}}gt-hidden{{end}}"> + <div class="ldap field {{if not (eq .type 2)}}tw-hidden{{end}}"> <label for="bind_dn">{{ctx.Locale.Tr "admin.auths.bind_dn"}}</label> <input id="bind_dn" name="bind_dn" value="{{.bind_dn}}" placeholder="cn=Search,dc=mydomain,dc=com"> </div> - <div class="ldap field {{if not (eq .type 2)}}gt-hidden{{end}}"> + <div class="ldap field {{if not (eq .type 2)}}tw-hidden{{end}}"> <label for="bind_password">{{ctx.Locale.Tr "admin.auths.bind_password"}}</label> <input id="bind_password" name="bind_password" type="password" autocomplete="off" value="{{.bind_password}}"> </div> @@ -38,7 +38,7 @@ <label for="user_base">{{ctx.Locale.Tr "admin.auths.user_base"}}</label> <input id="user_base" name="user_base" value="{{.user_base}}" placeholder="ou=Users,dc=mydomain,dc=com"> </div> - <div class="dldap required field {{if not (eq .type 5)}}gt-hidden{{end}}"> + <div class="dldap required field {{if not (eq .type 5)}}tw-hidden{{end}}"> <label for="user_dn">{{ctx.Locale.Tr "admin.auths.user_dn"}}</label> <input id="user_dn" name="user_dn" value="{{.user_dn}}" placeholder="uid=%s,ou=Users,dc=mydomain,dc=com"> </div> @@ -115,13 +115,13 @@ </div> <!-- ldap group end --> - <div class="ldap inline field {{if not (eq .type 2)}}gt-hidden{{end}}"> + <div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}"> <div class="ui checkbox"> <label for="use_paged_search"><strong>{{ctx.Locale.Tr "admin.auths.use_paged_search"}}</strong></label> <input id="use_paged_search" name="use_paged_search" class="use-paged-search" type="checkbox" {{if .use_paged_search}}checked{{end}}> </div> </div> - <div class="ldap field search-page-size required {{if or (not (eq .type 2)) (not .use_paged_search)}}gt-hidden{{end}}"> + <div class="ldap field search-page-size required {{if or (not (eq .type 2)) (not .use_paged_search)}}tw-hidden{{end}}"> <label for="search_page_size">{{ctx.Locale.Tr "admin.auths.search_page_size"}}</label> <input id="search_page_size" name="search_page_size" value="{{.search_page_size}}"> </div> 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 @@ -<div class="oauth2 field {{if not (eq .type 6)}}gt-hidden{{end}}"> +<div class="oauth2 field {{if not (eq .type 6)}}tw-hidden{{end}}"> <div class="inline required field"> <label>{{ctx.Locale.Tr "admin.auths.oauth2_provider"}}</label> <div class="ui selection type dropdown"> 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 @@ -<div class="smtp field {{if not (eq .type 3)}}gt-hidden{{end}}"> +<div class="smtp field {{if not (eq .type 3)}}tw-hidden{{end}}"> <div class="inline required field"> <label>{{ctx.Locale.Tr "admin.auths.smtp_auth"}}</label> <div class="ui selection type dropdown"> 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 @@ -<div class="sspi field {{if not (eq .type 7)}}gt-hidden{{end}}"> +<div class="sspi field {{if not (eq .type 7)}}tw-hidden{{end}}"> <div class="field"> <div class="ui checkbox"> <label for="sspi_auto_create_users"><strong>{{ctx.Locale.Tr "admin.auths.sspi_auto_create_users"}}</strong></label> 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 @@ -<div class="ui secondary filter menu gt-ac gt-mx-0"> - <form class="ui form ignore-dirty gt-f1"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> - </div> - </form> - <!-- Sort --> - <div class="ui dropdown type jump item gt-mr-0"> - <span class="text"> - {{ctx.Locale.Tr "repo.issues.filter_sort"}} - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> - <a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> - <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - </div> - </div> -</div> 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 @@ <dt>{{ctx.Locale.Tr "admin.config.mailer_user"}}</dt> <dd>{{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}</dd> <div class="divider"></div> - <dt class="gt-py-2">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt> + <dt class="tw-py-1">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt> <dd> <form class="ui form ignore-dirty" action="{{AppSubUrl}}/admin/config/test_mail" method="post"> {{.CsrfTokenHtml}} @@ -283,27 +283,6 @@ </dl> </div> - <h4 class="ui top attached header"> - {{ctx.Locale.Tr "admin.config.picture_config"}} - </h4> - <div class="ui attached table segment"> - <dl class="admin-dl-horizontal"> - <dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt> - <dd> - <div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}"> - <input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}> - </div> - </dd> - <div class="divider"></div> - <dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt> - <dd> - <div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}"> - <input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}> - </div> - </dd> - </dl> - </div> - <h4 class="ui top attached header"> {{ctx.Locale.Tr "admin.config.git_config"}} </h4> @@ -353,7 +332,7 @@ {{range $loggerName, $loggerDetail := .Loggers}} <dt>{{ctx.Locale.Tr "admin.config.logger_name_fmt" $loggerName}}</dt> {{if $loggerDetail.IsEnabled}} - <dd><pre class="gt-m-0">{{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}</pre></dd> + <dd><pre class="tw-m-0">{{$loggerDetail.EventWriters | JsonUtils.EncodeToString | JsonUtils.PrettyIndent}}</pre></dd> {{else}} <dd>{{ctx.Locale.Tr "admin.config.disabled_logger"}}</dd> {{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")}} +<h4 class="ui top attached header"> + {{ctx.Locale.Tr "admin.config.picture_config"}} +</h4> +<div class="ui attached table segment"> + <dl class="admin-dl-horizontal"> + <dt>{{ctx.Locale.Tr "admin.config.disable_gravatar"}}</dt> + <dd> + <div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_gravatar"}}"> + <input type="checkbox" data-config-dyn-key="picture.disable_gravatar" {{if .SystemConfig.Picture.DisableGravatar.Value ctx}}checked{{end}}><label></label> + </div> + </dd> + <div class="divider"></div> + <dt>{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}</dt> + <dd> + <div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}"> + <input type="checkbox" data-config-dyn-key="picture.enable_federated_avatar" {{if .SystemConfig.Picture.EnableFederatedAvatar.Value ctx}}checked{{end}}><label></label> + </div> + </dd> + </dl> +</div> + +<h4 class="ui top attached header"> + {{ctx.Locale.Tr "repository"}} +</h4> +<div class="ui attached segment"> + <form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/admin/config?key={{.SystemConfig.Repository.OpenWithEditorApps.DynKey}}"> + <div class="field"> + <details> + <summary>{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}}</summary> + <pre class="tw-px-4">{{.DefaultOpenWithEditorAppsString}}</pre> + </details> + </div> + <div class="field"> + <textarea name="value">{{(.SystemConfig.Repository.OpenWithEditorApps.Value ctx).ToTextareaString}}</textarea> + </div> + <div class="field"> + <button class="ui primary button">{{ctx.Locale.Tr "save"}}</button> + </div> + </form> +</div> +{{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 @@ </h4> <div class="ui attached table segment"> <form method="post" action="{{AppSubUrl}}/admin"> - <table class="ui very basic striped table unstackable gt-mb-0"> + <table class="ui very basic striped table unstackable tw-mb-0"> <thead> <tr> <th></th> diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index f43b4c5385..589fc5048a 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -2,16 +2,16 @@ <div class="admin-setting-content"> {{if .NeedUpdate}} <div class="ui negative message flash-error"> - <p>{{(ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer) | Str2html}}</p> + <p>{{ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer}}</p> </div> {{end}} <h4 class="ui top attached header"> - {{ctx.Locale.Tr "admin.dashboard.operations"}} + {{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}} </h4> <div class="ui attached table segment"> <form method="post" action="{{AppSubUrl}}/admin"> {{.CsrfTokenHtml}} - <table class="ui very basic table gt-mt-0 gt-px-4"> + <table class="ui very basic table tw-mt-0 tw-px-4"> <tbody> <tr> <td>{{ctx.Locale.Tr "admin.dashboard.delete_inactive_accounts"}}</td> @@ -75,69 +75,9 @@ <h4 class="ui top attached header"> {{ctx.Locale.Tr "admin.dashboard.system_status"}} </h4> - <div class="ui attached table segment"> - <dl class="admin-dl-horizontal"> - <dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt> - <dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt> - <dd>{{.SysStatus.NumGoroutine}}</dd> - <div class="divider"></div> - <dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt> - <dd>{{.SysStatus.MemAllocated}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt> - <dd>{{.SysStatus.MemTotal}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt> - <dd>{{.SysStatus.MemSys}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt> - <dd>{{.SysStatus.Lookups}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt> - <dd>{{.SysStatus.MemMallocs}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt> - <dd>{{.SysStatus.MemFrees}}</dd> - <div class="divider"></div> - <dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt> - <dd>{{.SysStatus.HeapAlloc}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt> - <dd>{{.SysStatus.HeapSys}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt> - <dd>{{.SysStatus.HeapIdle}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt> - <dd>{{.SysStatus.HeapInuse}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt> - <dd>{{.SysStatus.HeapReleased}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt> - <dd>{{.SysStatus.HeapObjects}}</dd> - <div class="divider"></div> - <dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt> - <dd>{{.SysStatus.StackInuse}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt> - <dd>{{.SysStatus.StackSys}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt> - <dd>{{.SysStatus.MSpanInuse}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt> - <dd>{{.SysStatus.MSpanSys}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt> - <dd>{{.SysStatus.MCacheInuse}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt> - <dd>{{.SysStatus.MCacheSys}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt> - <dd>{{.SysStatus.BuckHashSys}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt> - <dd>{{.SysStatus.GCSys}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt> - <dd>{{.SysStatus.OtherSys}}</dd> - <div class="divider"></div> - <dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt> - <dd>{{.SysStatus.NextGC}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt> - <dd>{{.SysStatus.LastGC}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt> - <dd>{{.SysStatus.PauseTotalNs}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt> - <dd>{{.SysStatus.PauseNs}}</dd> - <dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt> - <dd>{{.SysStatus.NumGC}}</dd> - </dl> + {{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}} + <div hx-get="{{$.Link}}/system_status" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".divider" class="ui attached table segment"> + {{template "admin/system_status" .}} </div> </div> {{template "admin/layout_footer" .}} diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index bcd80368e6..388863df9b 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -4,24 +4,21 @@ {{ctx.Locale.Tr "admin.emails.email_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) </h4> <div class="ui attached segment"> - <div class="ui secondary filter menu gt-ac gt-mx-0"> - <form class="ui form ignore-dirty gt-f1"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> - </div> + <div class="ui secondary filter menu tw-items-center tw-mx-0"> + <form class="ui form ignore-dirty tw-flex-1"> + {{template "shared/search/combo" dict "Value" .Keyword}} </form> <!-- Sort --> - <div class="ui dropdown type jump item gt-mr-0"> + <div class="ui dropdown type jump item tw-mr-0"> <span class="text"> {{ctx.Locale.Tr "repo.issues.filter_sort"}} </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if or (eq .SortType "email") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=email&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email"}}</a> - <a class="{{if eq .SortType "reverseemail"}}active {{end}}item" href="{{$.Link}}?sort=reverseemail&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email_reverse"}}</a> - <a class="{{if eq .SortType "username"}}active {{end}}item" href="{{$.Link}}?sort=username&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name"}}</a> - <a class="{{if eq .SortType "reverseusername"}}active {{end}}item" href="{{$.Link}}?sort=reverseusername&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name_reverse"}}</a> + <a class="{{if or (eq .SortType "email") (not .SortType)}}active {{end}}item" href="?sort=email&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email"}}</a> + <a class="{{if eq .SortType "reverseemail"}}active {{end}}item" href="?sort=reverseemail&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.email_reverse"}}</a> + <a class="{{if eq .SortType "username"}}active {{end}}item" href="?sort=username&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name"}}</a> + <a class="{{if eq .SortType "reverseusername"}}active {{end}}item" href="?sort=reverseusername&q={{$.Keyword}}">{{ctx.Locale.Tr "admin.emails.filter_sort.name_reverse"}}</a> </div> </div> </div> @@ -47,8 +44,8 @@ {{range .Emails}} <tr> <td><a href="{{AppSubUrl}}/{{.Name | PathEscape}}">{{.Name}}</a></td> - <td class="gt-ellipsis gt-max-width-12rem">{{.FullName}}</td> - <td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td> + <td class="gt-ellipsis tw-max-w-48">{{.FullName}}</td> + <td class="gt-ellipsis tw-max-w-48">{{.Email}}</td> <td>{{if .IsPrimary}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td> {{if .CanChange}} diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl index 0067f336e0..c1f5fb3314 100644 --- a/templates/admin/layout_head.tmpl +++ b/templates/admin/layout_head.tmpl @@ -1,9 +1,9 @@ {{template "base/head" .ctxData}} <div role="main" aria-label="{{.ctxData.Title}}" class="page-content {{.pageClass}}"> - <div class="ui container gt-mb-4"> + <div class="ui container"> {{template "base/alert" .ctxData}} </div> - <div class="ui container flex-container"> + <div class="ui container fluid padded flex-container"> {{template "admin/navbar" .ctxData}} <div class="flex-container-main"> {{/* block: admin-setting-content */}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index fa79f0f759..1b3b9d6efc 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -1,12 +1,18 @@ <div class="flex-container-nav"> <div class="ui fluid vertical menu"> <div class="header item">{{ctx.Locale.Tr "admin.settings"}}</div> - <a class="{{if .PageIsAdminDashboard}}active {{end}}item" href="{{AppSubUrl}}/admin"> - {{ctx.Locale.Tr "admin.dashboard"}} - </a> - <a class="{{if .PageIsAdminSelfCheck}}active {{end}}item" href="{{AppSubUrl}}/admin/self_check"> - {{ctx.Locale.Tr "admin.self_check"}} - </a> + + <details class="item toggleable-item" {{if or .PageIsAdminDashboard .PageIsAdminSelfCheck}}open{{end}}> + <summary>{{ctx.Locale.Tr "admin.maintenance"}}</summary> + <div class="menu"> + <a class="{{if .PageIsAdminDashboard}}active {{end}}item" href="{{AppSubUrl}}/admin"> + {{ctx.Locale.Tr "admin.dashboard"}} + </a> + <a class="{{if .PageIsAdminSelfCheck}}active {{end}}item" href="{{AppSubUrl}}/admin/self_check"> + {{ctx.Locale.Tr "admin.self_check"}} + </a> + </div> + </details> <details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}> <summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary> <div class="menu"> @@ -75,9 +81,17 @@ </div> </details> {{end}} - <a class="{{if .PageIsAdminConfig}}active {{end}}item" href="{{AppSubUrl}}/admin/config"> - {{ctx.Locale.Tr "admin.config"}} - </a> + <details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}> + <summary>{{ctx.Locale.Tr "admin.config"}}</summary> + <div class="menu"> + <a class="{{if .PageIsAdminConfigSummary}}active {{end}}item" href="{{AppSubUrl}}/admin/config"> + {{ctx.Locale.Tr "admin.config_summary"}} + </a> + <a class="{{if .PageIsAdminConfigSettings}}active {{end}}item" href="{{AppSubUrl}}/admin/config/settings"> + {{ctx.Locale.Tr "admin.config_settings"}} + </a> + </div> + </details> <a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/admin/notices"> {{ctx.Locale.Tr "admin.notices"}} </a> diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index ed410425b5..5ea003e5ec 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -17,10 +17,10 @@ <tbody> {{range .Notices}} <tr> - <td><div class="ui checkbox gt-df" data-id="{{.ID}}"><input type="checkbox"></div></td> + <td><div class="ui checkbox tw-flex" data-id="{{.ID}}"><input type="checkbox"></div></td> <td>{{.ID}}</td> <td>{{ctx.Locale.Tr .TrStr}}</td> - <td class="view-detail auto-ellipsis" style="width: 80%;"><span class="notice-description">{{.Description}}</span></td> + <td class="view-detail auto-ellipsis tw-w-4/5"><span class="notice-description">{{.Description}}</span></td> <td nowrap>{{DateTime "short" .CreatedUnix}}</td> <td class="view-detail"><a href="#">{{svg "octicon-note" 16}}</a></td> </tr> @@ -31,7 +31,7 @@ <tr> <th></th> <th colspan="5"> - <form class="gt-float-right" method="post" action="{{AppSubUrl}}/admin/notices/empty"> + <form class="tw-float-right" method="post" action="{{AppSubUrl}}/admin/notices/empty"> {{.CsrfTokenHtml}} <button type="submit" class="ui red small button">{{ctx.Locale.Tr "admin.notices.delete_all"}}</button> </form> @@ -49,8 +49,8 @@ </div> </div> </div> - <button class="ui small teal button" id="delete-selection" data-link="{{.Link}}/delete" data-redirect="{{.Link}}?page={{.Page.Paginater.Current}}"> - {{ctx.Locale.Tr "admin.notices.delete_selected"}} + <button class="ui small teal button" id="delete-selection" data-link="{{.Link}}/delete" data-redirect="?page={{.Page.Paginater.Current}}"> + <span class="text">{{ctx.Locale.Tr "admin.notices.delete_selected"}}</span> </button> </th> </tr> diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl index 0d79456b47..987ceab1e0 100644 --- a/templates/admin/org/list.tmpl +++ b/templates/admin/org/list.tmpl @@ -7,7 +7,26 @@ </div> </h4> <div class="ui attached segment"> - {{template "admin/base/search" .}} + <div class="ui secondary filter menu tw-items-center tw-mx-0"> + <form class="ui form ignore-dirty tw-flex-1"> + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}} + </form> + <!-- Sort --> + <div class="ui dropdown type jump item tw-mr-0"> + <span class="text"> + {{ctx.Locale.Tr "repo.issues.filter_sort"}} + </span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu"> + <a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "newest"}}active {{end}}item" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> + <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + </div> + </div> + </div> </div> <div class="ui attached table segment"> <table class="ui very basic striped table unstackable"> diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 5cfd9ddefa..863f11da25 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -13,16 +13,16 @@ </h4> <div class="ui attached segment"> <form class="ui form ignore-dirty"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Query}} - <select class="ui dropdown" name="type"> + <div class="ui small fluid action input"> + {{template "shared/search/input" dict "Value" .Query}} + <select class="ui small dropdown" name="type"> <option value="">{{ctx.Locale.Tr "packages.filter.type"}}</option> <option value="all">{{ctx.Locale.Tr "packages.filter.type.all"}}</option> {{range $type := .AvailableTypes}} <option{{if eq $.PackageType $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option> {{end}} </select> - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> + {{template "shared/search/button"}} </div> </form> </div> @@ -62,8 +62,8 @@ {{end}} </td> <td>{{.Package.Type.Name}}</td> - <td class="gt-ellipsis gt-max-width-12rem">{{.Package.Name}}</td> - <td class="gt-ellipsis gt-max-width-12rem"><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td> + <td class="gt-ellipsis tw-max-w-48">{{.Package.Name}}</td> + <td class="gt-ellipsis tw-max-w-48"><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td> <td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td> <td> {{if .Repository}} @@ -88,7 +88,7 @@ {{ctx.Locale.Tr "packages.settings.delete"}} </div> <div class="content"> - {{ctx.Locale.Tr "packages.settings.delete.notice" `<span class="name"></span>` `<span class="dataVersion"></span>` | Safe}} + {{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|SafeHTML) (`<span class="dataVersion"></span>`|SafeHTML)}} </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/admin/queue_manage.tmpl b/templates/admin/queue_manage.tmpl index 80214d1021..dc0196fc6a 100644 --- a/templates/admin/queue_manage.tmpl +++ b/templates/admin/queue_manage.tmpl @@ -30,7 +30,7 @@ - {{else}} {{$sum}} - <form action="{{$.Link}}/remove-all-items" method="post" class="gt-dib gt-ml-4"> + <form action="{{$.Link}}/remove-all-items" method="post" class="tw-inline-block tw-ml-4"> {{$.CsrfTokenHtml}} <button class="ui tiny basic red button">{{ctx.Locale.Tr "admin.monitor.queue.settings.remove_all_items"}}</button> </form> diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index fdba0734a2..4b27d87a45 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -7,7 +7,7 @@ </div> </h4> <div class="ui attached segment"> - {{template "admin/repo/search" .}} + {{template "shared/repo_search" .}} </div> <div class="ui attached table segment"> <table class="ui very basic striped table unstackable"> @@ -19,13 +19,13 @@ {{ctx.Locale.Tr "admin.repos.name"}} {{SortArrow "alphabetically" "reversealphabetically" $.SortType false}} </th> - <th>{{ctx.Locale.Tr "admin.repos.watches"}}</th> + <th>{{ctx.Locale.Tr "repo.watchers"}}</th> <th data-sortt-asc="moststars" data-sortt-desc="feweststars"> - {{ctx.Locale.Tr "admin.repos.stars"}} + {{ctx.Locale.Tr "repo.stars"}} {{SortArrow "moststars" "feweststars" $.SortType false}} </th> <th data-sortt-asc="mostforks" data-sortt-desc="fewestforks"> - {{ctx.Locale.Tr "admin.repos.forks"}} + {{ctx.Locale.Tr "repo.forks"}} {{SortArrow "mostforks" "fewestforks" $.SortType false}} </th> <th>{{ctx.Locale.Tr "admin.repos.issues"}}</th> @@ -101,7 +101,7 @@ </div> <div class="content"> <p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p> - {{ctx.Locale.Tr "repo.settings.delete_notices_2" `<span class="name"></span>` | Safe}}<br> + {{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|SafeHTML)}}<br> {{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}<br> </div> {{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/repo/search.tmpl b/templates/admin/repo/search.tmpl deleted file mode 100644 index 247ec5491a..0000000000 --- a/templates/admin/repo/search.tmpl +++ /dev/null @@ -1,29 +0,0 @@ -<div class="ui secondary filter menu gt-ac gt-mx-0"> - <form class="ui form ignore-dirty gt-f1"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> - </div> - </form> - <!-- Sort --> - <div class="ui dropdown type jump item gt-mr-0"> - <span class="text"> - {{ctx.Locale.Tr "repo.issues.filter_sort"}} - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> - <a class="{{if or (eq .SortType "oldest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> - <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - <a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?sort=moststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a> - <a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?sort=feweststars&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a> - <a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?sort=mostforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a> - <a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?sort=fewestforks&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a> - <a class="{{if eq .SortType "size"}}active {{end}}item" href="{{$.Link}}?sort=size&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</a> - <a class="{{if eq .SortType "reversesize"}}active {{end}}item" href="{{$.Link}}?sort=reversesize&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</a> - </div> - </div> -</div> diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index fb4f16791d..6a8e203694 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -8,10 +8,10 @@ </h4> <div class="ui attached segment"> <form class="ui form ignore-dirty"> - <div class="ui fluid action input"> - <input name="search" value="true" type="hidden"> - <input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.adopt_search"}}" autofocus> - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> + <div class="ui small fluid action input"> + <input name="search" value="true" type="hidden"> + <input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "repo.adopt_search"}}" autofocus> + {{template "shared/search/button"}} </div> </form> </div> @@ -20,10 +20,10 @@ {{if .Dirs}} <div class="ui aligned divided list"> {{range $dirI, $dir := .Dirs}} - <div class="item gt-df gt-ac"> - <span class="gt-f1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span> + <div class="item tw-flex tw-items-center"> + <span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span> <div> - <button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button> + <button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button> <div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}"> <div class="header"> <span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span> @@ -40,7 +40,7 @@ {{template "base/modal_actions_confirm"}} </form> </div> - <button class="ui button red show-modal gt-p-3" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button> + <button class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button> <div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}"> <div class="header"> <span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span> diff --git a/templates/admin/self_check.tmpl b/templates/admin/self_check.tmpl index 6bca01ec65..a6c2ac1ac9 100644 --- a/templates/admin/self_check.tmpl +++ b/templates/admin/self_check.tmpl @@ -4,33 +4,47 @@ <h4 class="ui top attached header"> {{ctx.Locale.Tr "admin.self_check"}} </h4> + + {{if .StartupProblems}} <div class="ui attached segment"> - {{if .DatabaseCheckHasProblems}} - {{if .DatabaseType.IsMySQL}} - <div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div> - {{else if .DatabaseType.IsMSSQL}} - <div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div> - {{end}} - {{if .DatabaseCheckCollationMismatch}} - <div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div> - {{end}} - {{if .DatabaseCheckCollationCaseInsensitive}} - <div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div> - {{end}} - {{if .DatabaseCheckInconsistentCollationColumns}} - <div class="ui red message"> - {{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}} - <ul class="gt-w-full"> - {{range .DatabaseCheckInconsistentCollationColumns}} - <li>{{.}}</li> - {{end}} - </ul> - </div> - {{end}} - {{else}} - <div class="gt-p-3">{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}</div> + <div class="ui warning message"> + <div>{{ctx.Locale.Tr "admin.self_check.startup_warnings"}}</div> + <ul class="tw-w-full">{{range .StartupProblems}}<li>{{.}}</li>{{end}}</ul> + </div> + </div> + {{end}} + + {{if .DatabaseCheckHasProblems}} + <div class="ui attached segment"> + {{if .DatabaseType.IsMySQL}} + <div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div> + {{else if .DatabaseType.IsMSSQL}} + <div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div> + {{end}} + {{if .DatabaseCheckCollationMismatch}} + <div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div> + {{end}} + {{if .DatabaseCheckCollationCaseInsensitive}} + <div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div> + {{end}} + {{if .DatabaseCheckInconsistentCollationColumns}} + <div class="ui red message"> + {{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}} + <ul class="tw-w-full"> + {{range .DatabaseCheckInconsistentCollationColumns}} + <li>{{.}}</li> + {{end}} + </ul> + </div> {{end}} </div> + {{end}} + + {{if and (not .StartupProblems) (not .DatabaseCheckHasProblems)}} + <div class="ui attached segment"> + {{ctx.Locale.Tr "admin.self_check.no_problem_found"}} + </div> + {{end}} </div> {{template "admin/layout_footer" .}} diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl index ffb8bf812f..694bf56d96 100644 --- a/templates/admin/stacktrace-row.tmpl +++ b/templates/admin/stacktrace-row.tmpl @@ -1,6 +1,6 @@ <div class="item"> - <div class="gt-df gt-ac"> - <div class="icon gt-ml-3 gt-mr-3"> + <div class="tw-flex tw-items-center"> + <div class="icon tw-ml-2 tw-mr-2"> {{if eq .Process.Type "request"}} {{svg "octicon-globe" 16}} {{else if eq .Process.Type "system"}} @@ -11,7 +11,7 @@ {{svg "octicon-code" 16}} {{end}} </div> - <div class="content gt-f1"> + <div class="content tw-flex-1"> <div class="header">{{.Process.Description}}</div> <div class="description">{{if ne .Process.Type "none"}}{{TimeSince .Process.Start ctx.Locale}}{{end}}</div> </div> @@ -22,14 +22,14 @@ </div> </div> {{if .Process.Stacks}} - <div class="divided list gt-ml-3"> + <div class="divided list tw-ml-2"> {{range .Process.Stacks}} <div class="item"> <details> <summary> <div class="flex-text-inline"> - <div class="header gt-ml-3"> - <span class="icon gt-mr-3">{{svg "octicon-code" 16}}</span>{{.Description}}{{if gt .Count 1}} * {{.Count}}{{end}} + <div class="header tw-ml-2"> + <span class="icon tw-mr-2">{{svg "octicon-code" 16}}</span>{{.Description}}{{if gt .Count 1}} * {{.Count}}{{end}} </div> <div class="description"> {{range .Labels}} @@ -40,9 +40,9 @@ </summary> <div class="list"> {{range .Entry}} - <div class="item gt-df gt-ac"> - <span class="icon gt-mr-4">{{svg "octicon-dot-fill" 16}}</span> - <div class="content gt-f1"> + <div class="item tw-flex tw-items-center"> + <span class="icon tw-mr-4">{{svg "octicon-dot-fill" 16}}</span> + <div class="content tw-flex-1"> <div class="header"><code>{{.Function}}</code></div> <div class="description"><code>{{.File}}:{{.Line}}</code></div> </div> diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index 894e41f8d7..e324570c96 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -1,11 +1,11 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}} <div class="admin-setting-content"> - <div class="gt-df gt-ac"> - <div class="gt-f1"> + <div class="tw-flex tw-items-center"> + <div class="tw-flex-1"> <div class="ui compact small menu"> - <a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="{{.Link}}?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a> - <a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="{{.Link}}?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a> + <a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a> + <a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a> </div> </div> <form target="_blank" action="{{AppSubUrl}}/admin/monitor/diagnosis" class="ui form"> @@ -39,7 +39,7 @@ {{ctx.Locale.Tr "admin.monitor.process.cancel"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" `<span class="name"></span>` | Safe}}</p> + <p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|SafeHTML)}}</p> <p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p> </div> {{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/system_status.tmpl b/templates/admin/system_status.tmpl new file mode 100644 index 0000000000..7b5c9be6cc --- /dev/null +++ b/templates/admin/system_status.tmpl @@ -0,0 +1,62 @@ +<dl class="admin-dl-horizontal"> + <dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt> + <dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt> + <dd>{{.SysStatus.NumGoroutine}}</dd> + <div class="divider"></div> + <dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt> + <dd>{{.SysStatus.MemAllocated}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt> + <dd>{{.SysStatus.MemTotal}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt> + <dd>{{.SysStatus.MemSys}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt> + <dd>{{.SysStatus.Lookups}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt> + <dd>{{.SysStatus.MemMallocs}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt> + <dd>{{.SysStatus.MemFrees}}</dd> + <div class="divider"></div> + <dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt> + <dd>{{.SysStatus.HeapAlloc}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt> + <dd>{{.SysStatus.HeapSys}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt> + <dd>{{.SysStatus.HeapIdle}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt> + <dd>{{.SysStatus.HeapInuse}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt> + <dd>{{.SysStatus.HeapReleased}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt> + <dd>{{.SysStatus.HeapObjects}}</dd> + <div class="divider"></div> + <dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt> + <dd>{{.SysStatus.StackInuse}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt> + <dd>{{.SysStatus.StackSys}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt> + <dd>{{.SysStatus.MSpanInuse}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt> + <dd>{{.SysStatus.MSpanSys}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt> + <dd>{{.SysStatus.MCacheInuse}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt> + <dd>{{.SysStatus.MCacheSys}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt> + <dd>{{.SysStatus.BuckHashSys}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt> + <dd>{{.SysStatus.GCSys}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt> + <dd>{{.SysStatus.OtherSys}}</dd> + <div class="divider"></div> + <dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt> + <dd>{{.SysStatus.NextGC}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt> + <dd><relative-time format="duration" datetime="{{.SysStatus.LastGCTime}}">{{.SysStatus.LastGCTime}}</relative-time></dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt> + <dd>{{.SysStatus.PauseTotalNs}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt> + <dd>{{.SysStatus.PauseNs}}</dd> + <dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt> + <dd>{{.SysStatus.NumGC}}</dd> +</dl> diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index fcb8ce0827..41b00defb4 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -53,7 +53,7 @@ </div> </div> - <div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}gt-hidden{{end}}"> + <div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}tw-hidden{{end}}"> <label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label> <input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus> </div> @@ -65,11 +65,26 @@ <label for="email">{{ctx.Locale.Tr "email"}}</label> <input id="email" name="email" type="email" value="{{.User.Email}}" autofocus required> </div> - <div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}gt-hidden{{end}}"> + <div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}tw-hidden{{end}}"> <label for="password">{{ctx.Locale.Tr "password"}}</label> <input id="password" name="password" type="password" autocomplete="new-password"> <p class="help">{{ctx.Locale.Tr "admin.users.password_helper"}}</p> </div> + + <div class="field {{if .Err_Language}}error{{end}}"> + <label for="language">{{ctx.Locale.Tr "settings.language"}}</label> + <div class="ui selection dropdown"> + <input name="language" type="hidden" value="{{.User.Language}}"> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="text">{{range .AllLangs}}{{if eq $.User.Language .Lang}}{{.Name}}{{end}}{{end}}</div> + <div class="menu"> + {{range .AllLangs}} + <div class="item{{if eq $.User.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div> + {{end}} + </div> + </div> + </div> + <div class="field {{if .Err_Website}}error{{end}}"> <label for="website">{{ctx.Locale.Tr "settings.website"}}</label> <input id="website" name="website" type="url" value="{{.User.Website}}" placeholder="http://mydomain.com or https://mydomain.com" maxlength="255"> @@ -113,13 +128,13 @@ <input name="restricted" type="checkbox" {{if .User.IsRestricted}}checked{{end}}> </div> </div> - <div class="inline field {{if DisableGitHooks}}gt-hidden{{end}}"> + <div class="inline field {{if DisableGitHooks}}tw-hidden{{end}}"> <div class="ui checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.users.allow_git_hook_tooltip"}}"> <label><strong>{{ctx.Locale.Tr "admin.users.allow_git_hook"}}</strong></label> <input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}} {{if DisableGitHooks}}disabled{{end}}> </div> </div> - <div class="inline field {{if or (DisableImportLocal) (.DisableMigrations)}}gt-hidden{{end}}"> + <div class="inline field {{if or (DisableImportLocal) (.DisableMigrations)}}tw-hidden{{end}}"> <div class="ui checkbox"> <label><strong>{{ctx.Locale.Tr "admin.users.allow_import_local"}}</strong></label> <input name="allow_import_local" type="checkbox" {{if .User.CanImportLocal}}checked{{end}} {{if DisableImportLocal}}disabled{{end}}> @@ -166,7 +181,7 @@ <label>{{ctx.Locale.Tr "settings.lookup_avatar_by_mail"}}</label> </div> </div> - <div class="field gt-pl-4 {{if .Err_Gravatar}}error{{end}}"> + <div class="field tw-pl-4 {{if .Err_Gravatar}}error{{end}}"> <label for="gravatar">Avatar {{ctx.Locale.Tr "email"}}</label> <input id="gravatar" name="gravatar" value="{{.User.AvatarEmail}}"> </div> @@ -179,7 +194,7 @@ </div> </div> - <div class="inline field gt-pl-4"> + <div class="inline field tw-pl-4"> <label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label> <input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp"> </div> diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 8fdc80fc70..528d047507 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -15,7 +15,7 @@ <div class="ui dropdown type jump item"> <span class="text">{{ctx.Locale.Tr "admin.users.list_status_filter.menu_text"}}</span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> + <div class="menu flex-items-menu"> <a class="item j-reset-status-filter">{{ctx.Locale.Tr "admin.users.list_status_filter.reset"}}</a> <div class="divider"></div> <label class="item"><input type="radio" name="status_filter[is_admin]" value="1"> {{ctx.Locale.Tr "admin.users.list_status_filter.is_admin"}}</label> @@ -52,11 +52,7 @@ </div> </div> - <!-- Search Text --> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> - </div> + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}} </form> </div> <div class="ui attached table segment"> @@ -96,7 +92,7 @@ <span class="ui mini label">{{ctx.Locale.Tr "admin.users.remote"}}</span> {{end}} </td> - <td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td> + <td class="gt-ellipsis tw-max-w-48">{{.Email}}</td> <td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> @@ -107,7 +103,7 @@ <td><span>{{ctx.Locale.Tr "admin.users.never_login"}}</span></td> {{end}} <td> - <div class="gt-df gt-gap-3"> + <div class="tw-flex tw-gap-2"> <a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.users.details"}}">{{svg "octicon-person"}}</a> <a href="{{$.Link}}/{{.ID}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a> </div> diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 81f70511d0..b04ebc4b60 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -26,7 +26,7 @@ <div class="inline field {{if .Err_Visibility}}error{{end}}"> <span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span> <div class="ui selection type dropdown"> - <input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{.visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}"> + <input type="hidden" id="visibility" name="visibility" value="{{if .visibility}}{{printf "%d" .visibility}}{{else}}{{printf "%d" .DefaultUserVisibilityMode}}{{end}}"> <div class="text"> {{if .DefaultUserVisibilityMode.IsPublic}}{{ctx.Locale.Tr "settings.visibility.public"}}{{end}} {{if .DefaultUserVisibilityMode.IsLimited}}{{ctx.Locale.Tr "settings.visibility.limited"}}{{end}} @@ -47,7 +47,7 @@ </div> </div> - <div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}gt-hidden{{end}}"> + <div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}tw-hidden{{end}}"> <label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label> <input id="login_name" name="login_name" value="{{.login_name}}"> </div> @@ -59,12 +59,12 @@ <label for="email">{{ctx.Locale.Tr "email"}}</label> <input id="email" name="email" type="email" value="{{.email}}" required> </div> - <div class="required local field {{if .Err_Password}}error{{end}} {{if not (eq .login_type "0-0")}}gt-hidden{{end}}"> + <div class="required local field {{if .Err_Password}}error{{end}} {{if not (eq .login_type "0-0")}}tw-hidden{{end}}"> <label for="password">{{ctx.Locale.Tr "password"}}</label> <input id="password" name="password" type="password" autocomplete="new-password" value="{{.password}}" {{if eq .login_type "0-0"}}required{{end}}> </div> - <div class="inline field local {{if ne .login_type "0-0"}}gt-hidden{{end}}"> + <div class="inline field local {{if ne .login_type "0-0"}}tw-hidden{{end}}"> <div class="ui checkbox"> <label><strong>{{ctx.Locale.Tr "auth.allow_password_change"}}</strong></label> <input name="must_change_password" type="checkbox" checked> diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl index fd3017607c..21943a8382 100644 --- a/templates/admin/user/view.tmpl +++ b/templates/admin/user/view.tmpl @@ -2,7 +2,7 @@ <div class="admin-setting-content"> <div class="admin-responsive-columns"> - <div class="gt-f1"> + <div class="tw-flex-1"> <h4 class="ui top attached header"> {{.Title}} <div class="ui right"> @@ -13,7 +13,7 @@ {{template "admin/user/view_details" .}} </div> </div> - <div class="gt-f1"> + <div class="tw-flex-1"> <h4 class="ui top attached header"> {{ctx.Locale.Tr "admin.emails"}} <div class="ui right"> diff --git a/templates/admin/user/view_details.tmpl b/templates/admin/user/view_details.tmpl index 21425eecb4..be2f32b5ec 100644 --- a/templates/admin/user/view_details.tmpl +++ b/templates/admin/user/view_details.tmpl @@ -48,6 +48,14 @@ {{svg "octicon-x"}} {{end}} </div> + {{if .User.Language}} + <div class="flex-item-body"> + <span class="flex-text-inline"> + <b>{{ctx.Locale.Tr "settings.language"}}:</b> + {{range .AllLangs}}{{if eq $.User.Language .Lang}}{{.Name}}{{end}}{{end}} + </span> + </div> + {{end}} {{if .User.Location}} <div class="flex-item-body"> <span class="flex-text-inline">{{svg "octicon-location"}}{{.User.Location}}</span> diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl index 160584f769..760d3bfa2c 100644 --- a/templates/base/alert.tmpl +++ b/templates/base/alert.tmpl @@ -1,20 +1,20 @@ {{if .Flash.ErrorMsg}} <div class="ui negative message flash-message flash-error"> - <p>{{.Flash.ErrorMsg | Str2html}}</p> + <p>{{.Flash.ErrorMsg | SanitizeHTML}}</p> </div> {{end}} {{if .Flash.SuccessMsg}} <div class="ui positive message flash-message flash-success"> - <p>{{.Flash.SuccessMsg | Str2html}}</p> + <p>{{.Flash.SuccessMsg | SanitizeHTML}}</p> </div> {{end}} {{if .Flash.InfoMsg}} <div class="ui info message flash-message flash-info"> - <p>{{.Flash.InfoMsg | Str2html}}</p> + <p>{{.Flash.InfoMsg | SanitizeHTML}}</p> </div> {{end}} {{if .Flash.WarningMsg}} <div class="ui warning message flash-message flash-warning"> - <p>{{.Flash.WarningMsg | Str2html}}</p> + <p>{{.Flash.WarningMsg | SanitizeHTML}}</p> </div> {{end}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 1d7ec15dc0..6801c8240f 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -2,6 +2,6 @@ <details> <summary>{{.Summary}}</summary> <code> - {{.Details | Str2html}} + {{.Details | SanitizeHTML}} </code> </details> diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index d65a3626a4..fed426a469 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -16,6 +16,5 @@ <script src="{{AssetUrlPrefix}}/js/index.js?v={{AssetVersion}}" onerror="alert('Failed to load asset files from ' + this.src + '. Please make sure the asset files can be accessed.')"></script> {{template "custom/footer" .}} - {{ctx.DataRaceCheck $.Context}} </body> </html> diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl index f0a7865602..8d0d8e669c 100644 --- a/templates/base/footer_content.tmpl +++ b/templates/base/footer_content.tmpl @@ -1,6 +1,8 @@ <footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}"> <div class="left-links" role="contentinfo" aria-label="{{ctx.Locale.Tr "aria.footer.software"}}"> - <a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">{{ctx.Locale.Tr "powered_by" "Gitea"}}</a> + {{if ShowFooterPoweredBy}} + <a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">{{ctx.Locale.Tr "powered_by" "Gitea"}}</a> + {{end}} {{if (or .ShowFooterVersion .PageIsAdmin)}} {{ctx.Locale.Tr "version"}}: {{if .IsAdmin}} diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index b9c050fdd5..2de8f58235 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -2,7 +2,7 @@ <html lang="{{ctx.Locale.Lang}}" data-theme="{{ThemeName .SignedUser}}"> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title> + <title>{{if .Title}}{{.Title}} - {{end}}{{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}}</title> {{if .ManifestData}}<link rel="manifest" href="data:{{.ManifestData}}">{{end}} <meta name="author" content="{{if .Repository}}{{.Owner.Name}}{{else}}{{MetaAuthor}}{{end}}"> <meta name="description" content="{{if .Repository}}{{.Repository.Name}}{{if .Repository.Description}} - {{.Repository.Description}}{{end}}{{else}}{{MetaDescription}}{{end}}"> @@ -29,8 +29,7 @@ {{template "base/head_style" .}} {{template "custom/header" .}} </head> -<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-push-url="false"> - {{ctx.DataRaceCheck $.Context}} +<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false"> {{template "custom/body_outer_pre" .}} <div class="full height"> diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index effe4dcea9..addff22c49 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -11,16 +11,16 @@ </a> <!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column --> - <div class="ui secondary menu item navbar-mobile-right"> + <div class="ui secondary menu item navbar-mobile-right only-mobile"> {{if .IsSigned}} - <a id="mobile-notifications-icon" class="item gt-w-auto gt-p-3" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> - <div class="gt-relative"> + <a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> + <div class="tw-relative"> {{svg "octicon-bell"}} - <span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span> + <span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span> </div> </a> {{end}} - <button class="item gt-w-auto ui icon mini button gt-p-3 gt-m-0" id="navbar-expand-toggle">{{svg "octicon-three-bars"}}</button> + <button class="item tw-w-auto ui icon mini button tw-p-2 tw-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button> </div> <!-- navbar links non-mobile --> @@ -56,9 +56,9 @@ <div class="navbar-right ui secondary menu"> {{if and .IsSigned .MustChangePassword}} <div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}"> - <span class="text gt-df gt-ac"> - {{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}} - <span class="mobile-only gt-ml-3">{{.SignedUser.Name}}</span> + <span class="text tw-flex tw-items-center"> + {{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}} + <span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span> <span class="not-mobile">{{svg "octicon-triangle-down"}}</span> </span> <div class="menu user-menu"> @@ -75,19 +75,19 @@ </div><!-- end dropdown avatar menu --> {{else if .IsSigned}} {{if EnableTimetracking}} - <a class="active-stopwatch-trigger item gt-mx-0{{if not .ActiveStopwatch}} gt-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}"> - <div class="gt-relative"> + <a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}"> + <div class="tw-relative"> {{svg "octicon-stopwatch"}} <span class="header-stopwatch-dot"></span> </div> - <span class="mobile-only gt-ml-3">{{ctx.Locale.Tr "active_stopwatch"}}</span> + <span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span> </a> - <div class="active-stopwatch-popup item tippy-target gt-p-3"> - <div class="gt-df gt-ac"> - <a class="stopwatch-link gt-df gt-ac" href="{{.ActiveStopwatch.IssueLink}}"> - {{svg "octicon-issue-opened" 16 "gt-mr-3"}} + <div class="active-stopwatch-popup item tippy-target tw-p-2"> + <div class="tw-flex tw-items-center"> + <a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}"> + {{svg "octicon-issue-opened" 16 "tw-mr-2"}} <span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span> - <span class="ui primary label stopwatch-time gt-my-0 gt-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}"> + <span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}"> {{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}} </span> </a> @@ -111,18 +111,18 @@ </div> {{end}} - <a class="item not-mobile gt-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> - <div class="gt-relative"> + <a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> + <div class="tw-relative"> {{svg "octicon-bell"}} - <span class="notification_count{{if not $notificationUnreadCount}} gt-hidden{{end}}">{{$notificationUnreadCount}}</span> + <span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span> </div> </a> - <div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}"> + <div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}"> <span class="text"> {{svg "octicon-plus"}} <span class="not-mobile">{{svg "octicon-triangle-down"}}</span> - <span class="mobile-only">{{ctx.Locale.Tr "create_new"}}</span> + <span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span> </span> <div class="menu"> <a class="item" href="{{AppSubUrl}}/repo/create"> @@ -141,10 +141,10 @@ </div><!-- end content create new menu --> </div><!-- end dropdown menu create new --> - <div class="ui dropdown jump item gt-mx-0 gt-pr-3" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}"> - <span class="text gt-df gt-ac"> - {{ctx.AvatarUtils.Avatar .SignedUser 24 "gt-mr-2"}} - <span class="mobile-only gt-ml-3">{{.SignedUser.Name}}</span> + <div class="ui dropdown jump item tw-mx-0 tw-pr-2" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}"> + <span class="text tw-flex tw-items-center"> + {{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}} + <span class="only-mobile tw-ml-2">{{.SignedUser.Name}}</span> <span class="not-mobile">{{svg "octicon-triangle-down"}}</span> </span> <div class="menu user-menu"> diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 4a723f63b9..22e08e9c8f 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -41,6 +41,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. remove_label_str: {{ctx.Locale.Tr "remove_label_str"}}, modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, modal_cancel: {{ctx.Locale.Tr "modal.cancel"}}, + more_items: {{ctx.Locale.Tr "more_items"}}, }, }; {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} diff --git a/templates/base/markup_codepreview.tmpl b/templates/base/markup_codepreview.tmpl new file mode 100644 index 0000000000..a1a4f26b47 --- /dev/null +++ b/templates/base/markup_codepreview.tmpl @@ -0,0 +1,25 @@ +<div class="code-preview-container file-content"> + <div class="code-preview-header"> + <a href="{{.FullURL}}" class="muted" rel="nofollow">{{.FilePath}}</a> + {{$link := HTMLFormat `<a href="%s/src/commit/%s" rel="nofollow">%s</a>` .RepoLink .CommitID (.CommitID | ShortSha) -}} + {{- if eq .LineStart .LineStop -}} + {{ctx.Locale.Tr "repo.code_preview_line_in" .LineStart $link}} + {{- else -}} + {{ctx.Locale.Tr "repo.code_preview_line_from_to" .LineStart .LineStop $link}} + {{- end}} + </div> + <table class="file-view"> + <tbody> + {{- range $idx, $line := .HighlightLines -}} + <tr> + <td class="lines-num"><span data-line-number="{{$line.Num}}"></span></td> + {{- if $.EscapeStatus.Escaped -}} + {{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}} + <td class="lines-escape">{{if $lineEscapeStatus.Escaped}}<a href="#" class="toggle-escape-button btn interact-bg" title="{{if $lineEscapeStatus.HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if $lineEscapeStatus.HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></a>{{end}}</td> + {{- end}} + <td class="lines-code chroma"><div class="code-inner">{{$line.FormattedContent}}</div></td>{{/* only div works, span generates incorrect HTML structure */}} + </tr> + {{- end -}} + </tbody> + </table> +</div> diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl index 503817c339..9a7a6322f7 100644 --- a/templates/base/paginate.tmpl +++ b/templates/base/paginate.tmpl @@ -1,30 +1,32 @@ -{{$paginationLink := .Page.GetParams}} +{{$paginationParams := .Page.GetParams}} +{{$paginationLink := $.Link}} +{{if eq $paginationLink AppSubUrl}}{{$paginationLink = print $paginationLink "/"}}{{end}} {{with .Page.Paginater}} {{if gt .TotalPages 1}} <div class="center page buttons"> <div class="ui borderless pagination menu"> - <a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$.Link}}{{if $paginationLink}}?{{$paginationLink}}{{end}}"{{end}}> - {{svg "gitea-double-chevron-left" 16 "gt-mr-2"}} + <a class="{{if .IsFirst}}disabled{{end}} item navigation" {{if not .IsFirst}}href="{{$paginationLink}}{{if $paginationParams}}?{{$paginationParams}}{{end}}"{{end}}> + {{svg "gitea-double-chevron-left" 16 "tw-mr-1"}} <span class="navigation_label">{{ctx.Locale.Tr "admin.first_page"}}</span> </a> - <a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$.Link}}?page={{.Previous}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}> - {{svg "octicon-chevron-left" 16 "gt-mr-2"}} + <a class="{{if not .HasPrevious}}disabled{{end}} item navigation" {{if .HasPrevious}}href="{{$paginationLink}}?page={{.Previous}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}> + {{svg "octicon-chevron-left" 16 "tw-mr-1"}} <span class="navigation_label">{{ctx.Locale.Tr "repo.issues.previous"}}</span> </a> {{range .Pages}} {{if eq .Num -1}} <a class="disabled item">...</a> {{else}} - <a class="{{if .IsCurrent}}active {{end}}item gt-content-center" {{if not .IsCurrent}}href="{{$.Link}}?page={{.Num}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}>{{.Num}}</a> + <a class="{{if .IsCurrent}}active {{end}}item tw-content-center" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a> {{end}} {{end}} - <a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$.Link}}?page={{.Next}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}> + <a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$paginationLink}}?page={{.Next}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}> <span class="navigation_label">{{ctx.Locale.Tr "repo.issues.next"}}</span> - {{svg "octicon-chevron-right" 16 "gt-ml-2"}} + {{svg "octicon-chevron-right" 16 "tw-ml-1"}} </a> - <a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$.Link}}?page={{.TotalPages}}{{if $paginationLink}}&{{$paginationLink}}{{end}}"{{end}}> + <a class="{{if .IsLast}}disabled{{end}} item navigation" {{if not .IsLast}}href="{{$paginationLink}}?page={{.TotalPages}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}> <span class="navigation_label">{{ctx.Locale.Tr "admin.last_page"}}</span> - {{svg "gitea-double-chevron-right" 16 "gt-ml-2"}} + {{svg "gitea-double-chevron-right" 16 "tw-ml-1"}} </a> </div> </div> diff --git a/templates/code/searchcombo.tmpl b/templates/code/searchcombo.tmpl deleted file mode 100644 index 48dc13b47b..0000000000 --- a/templates/code/searchcombo.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -{{template "code/searchform" .}} -<div class="divider"></div> -<div class="ui user list"> - {{if .CodeIndexerUnavailable}} - <div class="ui error message"> - <p>{{ctx.Locale.Tr "explore.code_search_unavailable"}}</p> - </div> - {{else if .SearchResults}} - <h3> - {{ctx.Locale.Tr "explore.code_search_results" (.Keyword|Escape) | Str2html}} - </h3> - {{template "code/searchresults" .}} - {{else if .Keyword}} - <div>{{ctx.Locale.Tr "explore.code_no_results"}}</div> - {{end}} -</div> -{{template "base/paginate" .}} diff --git a/templates/code/searchform.tmpl b/templates/code/searchform.tmpl deleted file mode 100644 index fae1340046..0000000000 --- a/templates/code/searchform.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -<form class="ui form ignore-dirty"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable}} - <div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "explore.search.type.tooltip"}}"> - <input name="t" type="hidden" value="{{.queryType}}"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="text">{{ctx.Locale.Tr (printf "explore.search.%s" (or .queryType "fuzzy"))}}</div> - <div class="menu"> - <div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "explore.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "explore.search.fuzzy"}}</div> - <div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "explore.search.match.tooltip"}}">{{ctx.Locale.Tr "explore.search.match"}}</div> - </div> - </div> - <button class="ui primary button"{{if .CodeIndexerUnavailable}} disabled{{end}}>{{ctx.Locale.Tr "explore.search"}}</button> - </div> -</form> diff --git a/templates/code/searchresults.tmpl b/templates/code/searchresults.tmpl deleted file mode 100644 index bb21a5e0dc..0000000000 --- a/templates/code/searchresults.tmpl +++ /dev/null @@ -1,43 +0,0 @@ -<div class="flex-text-block gt-fw"> - {{range $term := .SearchResultLanguages}} - <a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0" href="{{AppSubUrl}}{{if $.ContextUser}}/{{$.ContextUser.Name}}/-/code{{else}}/explore/code{{end}}?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}"> - <i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i> - {{$term.Language}} - <div class="detail">{{$term.Count}}</div> - </a> - {{end}} -</div> -<div class="repository search"> - {{range $result := .SearchResults}} - {{$repo := (index $.RepoMaps .RepoID)}} - <div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result"> - <h4 class="ui top attached normal header gt-df gt-fw"> - <span class="file gt-f1"> - <a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a> - {{if $repo.IsArchived}} - <span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span> - {{end}} - - {{.Filename}} - </span> - <a role="button" class="ui basic tiny button" rel="nofollow" href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> - </h4> - <div class="ui attached table segment"> - <div class="file-body file-code code-view"> - <table> - <tbody> - <tr> - <td class="lines-num"> - {{range .LineNumbers}} - <a href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{$result.Filename | PathEscapeSegments}}#L{{.}}"><span>{{.}}</span></a> - {{end}} - </td> - <td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td> - </tr> - </tbody> - </table> - </div> - </div> - {{template "shared/searchbottom" dict "root" $ "result" .}} - </div> - {{end}} -</div> diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl index 70844a8751..7b0bbba554 100644 --- a/templates/devtest/fetch-action.tmpl +++ b/templates/devtest/fetch-action.tmpl @@ -26,7 +26,7 @@ <div><button name="btn">submit post</button></div> </form> <form method="post" action="/no-such-uri" class="form-fetch-action"> - <div class="gt-py-5">bad action url</div> + <div class="tw-py-8">bad action url</div> <div><button name="btn">submit test</button></div> </form> </div> diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl index c8584c110b..015ab1e154 100644 --- a/templates/devtest/flex-list.tmpl +++ b/templates/devtest/flex-list.tmpl @@ -25,7 +25,7 @@ </div> <div class="flex-item-trailing"> <button class="ui tiny red button"> - {{svg "octicon-warning" 14}} CJK文本测试 + {{svg "octicon-alert" 14}} CJK文本测试 </button> <button class="ui tiny primary button"> {{svg "octicon-info" 14}} Button @@ -54,7 +54,7 @@ </div> <div class="flex-item-trailing"> <button class="ui tiny red button"> - {{svg "octicon-warning" 12}} CJK文本测试 <!-- single CJK text test, it shouldn't be horizontal --> + {{svg "octicon-alert" 12}} CJK文本测试 <!-- single CJK text test, it shouldn't be horizontal --> </button> </div> </div> @@ -73,7 +73,7 @@ </div> <div class="flex-item-trailing"> <a class="muted" href="{{$.Link}}"> - <span class="flex-text-inline"><i class="color-icon gt-mr-3" style="background-color: aqua"></i>Go</span> + <span class="flex-text-inline"><i class="color-icon tw-mr-2 tw-bg-blue"></i>Go</span> </a> <a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-star" 16}}45000</a> <a class="text grey flex-text-inline" href="{{$.Link}}">{{svg "octicon-git-branch" 16}}1234</a> @@ -104,7 +104,7 @@ </div> <h1>If parent provides the padding/margin space:</h1> - <div class="gt-border-secondary gt-py-4"> + <div class="tw-border tw-border-secondary tw-py-4"> <div class="flex-list flex-space-fitted"> <div class="flex-item">item 1 (no padding top)</div> <div class="flex-item">item 2 (no padding bottom)</div> diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/fomantic-modal.tmpl index eda169a043..5cd36721a7 100644 --- a/templates/devtest/fomantic-modal.tmpl +++ b/templates/devtest/fomantic-modal.tmpl @@ -5,7 +5,7 @@ <div id="test-modal-form-1" class="ui mini modal"> <div class="header">Form dialog (layout 1)</div> <form class="content" method="post"> - <div class="ui input gt-w-full"><input name="user_input"></div> + <div class="ui input tw-w-full"><input name="user_input"></div> {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} </form> </div> @@ -14,7 +14,7 @@ <div class="header">Form dialog (layout 2)</div> <form method="post"> <div class="content"> - <div class="ui input gt-w-full"><input name="user_input"></div> + <div class="ui input tw-w-full"><input name="user_input"></div> {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} </div> </form> @@ -24,7 +24,7 @@ <div class="header">Form dialog (layout 3)</div> <form method="post"> <div class="content"> - <div class="ui input gt-w-full"><input name="user_input"></div> + <div class="ui input tw-w-full"><input name="user_input"></div> </div> {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} </form> @@ -33,7 +33,7 @@ <div id="test-modal-form-4" class="ui mini modal"> <div class="header">Form dialog (layout 4)</div> <div class="content"> - <div class="ui input gt-w-full"><input name="user_input"></div> + <div class="ui input tw-w-full"><input name="user_input"></div> </div> <form method="post"> {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} @@ -73,7 +73,7 @@ {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" "I know and must do this is dangerous operation")}} </div> - <div class="modal-buttons flex-text-block gt-fw"></div> + <div class="modal-buttons flex-text-block tw-flex-wrap"></div> <script type="module"> for (const el of $('.ui.modal')) { const $btn = $('<button>').text(`${el.id}`).on('click', () => { diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl index 73293ddf48..bb4fc77a74 100644 --- a/templates/devtest/gitea-ui.tmpl +++ b/templates/devtest/gitea-ui.tmpl @@ -2,6 +2,13 @@ <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}"> <div class="page-content devtest ui container"> <div> + <h1>Link</h1> + <div> + <a href="#">normal</a> + <a class="muted" href="#">muted</a> + <a class="suppressed" href="#">suppressed</a> + <a class="silenced" href="#">silenced</a> + </div> <h1>Button</h1> <div> Style: @@ -60,10 +67,10 @@ </li> <li class="sample-group"> <h2>Inline / Plain:</h2> - <div class="gt-my-2"> - <button class="btn gt-p-3">Plain button</button> - <button class="btn interact-fg gt-p-3">Plain button with interact fg</button> - <button class="btn interact-bg gt-p-3">Plain button with interact bg</button> + <div class="tw-my-1"> + <button class="btn tw-p-2">Plain button</button> + <button class="btn interact-fg tw-p-2">Plain button with interact fg</button> + <button class="btn interact-bg tw-p-2">Plain button with interact bg</button> </div> </li> </ul> @@ -95,8 +102,8 @@ <div> <h1>Loading</h1> - <div class="is-loading small-loading-icon gt-border-secondary gt-py-2"><span>loading ...</span></div> - <div class="is-loading gt-border-secondary gt-py-4"> + <div class="is-loading loading-icon-2px tw-border tw-border-secondary tw-py-1"><span>loading ...</span></div> + <div class="is-loading tw-border tw-border-secondary tw-py-4"> <p>loading ...</p> <p>loading ...</p> <p>loading ...</p> @@ -105,9 +112,46 @@ </div> <div> - <h1>GiteaOriginUrl</h1> - <div><gitea-origin-url data-url="test/url"></gitea-origin-url></div> - <div><gitea-origin-url data-url="/test/url"></gitea-origin-url></div> + <h1><origin-url></h1> + <div><origin-url data-url="test/url"></origin-url></div> + <div><origin-url data-url="/test/url"></origin-url></div> + </div> + + <div> + <h1><overflow-menu></h1> + <overflow-menu class="ui secondary pointing tabular borderless menu"> + <div class="overflow-menu-items"> + <a class="active item">item</a> + <a class="item">item 1</a> + <a class="item">item 2</a> + <a class="item">item 3</a> + <a class="item">item 4</a> + <a class="item">item 5</a> + <a class="item">item 6</a> + <a class="item">item 7</a> + <a class="item">item 8</a> + <a class="item">item 9</a> + <a class="item">item 10</a> + <a class="item">item 11</a> + <a class="item">item 12</a> + <a class="item">item 13</a> + <a class="item">item 14</a> + <a class="item">item 15</a> + <a class="item">item 16</a> + <a class="item">item 17</a> + <a class="item">item 18</a> + </div> + </overflow-menu> + </div> + + <div> + <h1>GiteaAbsoluteDate</h1> + <div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="short"></absolute-date></div> + <div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="long"></absolute-date></div> + <div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric"></absolute-date></div> + <div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div> + <div><absolute-date date="2024-03-11T19:00:00-05:00" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div> + <div class="tw-text-text-light-2">relative-time: <relative-time format="datetime" datetime="2024-03-11" year="" day="numeric" month="numeric"></relative-time></div> </div> <div> @@ -167,12 +211,13 @@ <h2>Dropdown with SVG</h2> <div> - <div class="ui dropdown" style="border: 1px red dashed" data-tooltip-content="border for demo purpose only"> - <span class="text">simple</span> + <div class="ui dropdown tw-border tw-border-red tw-border-dashed" data-tooltip-content="border for demo purpose only"> + <span class="text">search-input & flex-item in menu</span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> + <div class="menu flex-items-menu"> <div class="ui icon search input"><i class="icon">{{svg "octicon-search"}}</i><input type="text" value="search input in menu"></div> - <div class="item">item</div> + <div class="item"><input type="radio">item</div> + <div class="item"><input type="radio">item</div> </div> </div> <div class="ui search selection dropdown"> @@ -275,6 +320,12 @@ <div>ps: no JS code attached, so just a layout</div> {{template "shared/combomarkdowneditor" .}} </div> + + <h1>Tailwind CSS Demo</h1> + <div> + <button class="{{if true}}tw-bg-red{{end}} tw-p-5 tw-border tw-rounded hover:tw-bg-blue active:tw-bg-yellow">Button</button> + </div> + <script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script> </div> {{template "base/footer" .}} diff --git a/templates/devtest/tmplerr.tmpl b/templates/devtest/tmplerr.tmpl index 2fe3f1effd..dd938c895e 100644 --- a/templates/devtest/tmplerr.tmpl +++ b/templates/devtest/tmplerr.tmpl @@ -1,10 +1,10 @@ {{template "base/head" .}} <div class="page-content devtest"> - <div class="gt-df"> - <div style="width: 80%; "> + <div class="tw-flex"> + <div class="tw-w-4/5"> hello hello hello hello hello hello hello hello hello hello </div> - <div style="width: 20%;"> + <div class="tw-w-1/5"> {{template "devtest/tmplerr-sub" .}} </div> </div> diff --git a/templates/explore/code.tmpl b/templates/explore/code.tmpl index 2298575887..039933fa2d 100644 --- a/templates/explore/code.tmpl +++ b/templates/explore/code.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content explore users"> {{template "explore/navbar" .}} <div class="ui container"> - {{template "code/searchcombo" .}} + {{template "shared/search/code/search" .}} </div> </div> {{template "base/footer" .}} diff --git a/templates/explore/navbar.tmpl b/templates/explore/navbar.tmpl index 7f2aea497a..8e619fa66f 100644 --- a/templates/explore/navbar.tmpl +++ b/templates/explore/navbar.tmpl @@ -1,5 +1,5 @@ -<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar"> - <div class="new-menu-inner"> +<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav"> + <div class="overflow-menu-items tw-justify-center"> <a class="{{if .PageIsExploreRepositories}}active {{end}}item" href="{{AppSubUrl}}/explore/repos"> {{svg "octicon-repo"}} {{ctx.Locale.Tr "explore.repos"}} </a> @@ -17,4 +17,4 @@ </a> {{end}} </div> -</div> +</overflow-menu> diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index c51dcaa3ff..d00773a963 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -30,16 +30,23 @@ {{end}} </span> </div> - <div class="flex-item-trailing"> + <div class="flex-item-trailing muted-links"> {{if .PrimaryLanguage}} - <a class="muted" href="{{$.Link}}?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}"> - <span class="flex-text-inline"><i class="color-icon gt-mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span> + <a class="flex-text-inline" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}"> + <i class="color-icon tw-mr-2" style="background-color: {{.PrimaryLanguage.Color}}"></i> + {{.PrimaryLanguage.Language}} </a> {{end}} {{if not $.DisableStars}} - <a class="text grey flex-text-inline" href="{{.Link}}/stars">{{svg "octicon-star" 16}}{{.NumStars}}</a> + <a class="flex-text-inline" href="{{.Link}}/stars"> + <span aria-label="{{ctx.Locale.Tr "repo.stars"}}">{{svg "octicon-star" 16}}</span> + <span {{if ge .NumStars 1000}}data-tooltip-content="{{.NumStars}}"{{end}}>{{CountFmt .NumStars}}</span> + </a> {{end}} - <a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a> + <a class="flex-text-inline" href="{{.Link}}/forks"> + <span aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span> + <span {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}>{{CountFmt .NumForks}}</span> + </a> </div> </div> {{$description := .DescriptionHTML $.Context}} @@ -58,7 +65,7 @@ </div> {{else}} <div> - {{ctx.Locale.Tr "explore.repo_no_results"}} + {{ctx.Locale.Tr "search.no_results"}} </div> {{end}} </div> diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl deleted file mode 100644 index eaf2e7a090..0000000000 --- a/templates/explore/repo_search.tmpl +++ /dev/null @@ -1,42 +0,0 @@ -<div class="ui secondary filter menu gt-ac gt-mx-0"> - <form class="ui form ignore-dirty gt-f1"> - <input type="hidden" name="sort" value="{{$.SortType}}"> - <input type="hidden" name="language" value="{{$.Language}}"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword}} - {{if .PageIsExploreRepositories}} - <input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}"> - {{else if .TabName}} - <input type="hidden" name="tab" value="{{.TabName}}"> - {{end}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> - </div> - </form> - <!-- Sort --> - <div class="ui dropdown type jump item gt-mr-0"> - <span class="text"> - {{ctx.Locale.Tr "repo.issues.filter_sort"}} - </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="menu"> - <a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=newest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=oldest&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=alphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> - <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=reversealphabetically&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=recentupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=leastupdate&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - {{if not .DisableStars}} - <a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=moststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a> - <a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=feweststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a> - {{end}} - <a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a> - <a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a> - </div> - </div> -</div> -{{if and .PageIsExploreRepositories .OnlyShowRelevant}} - <div class="ui message explore-relevancy-note"> - <span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}">{{ctx.Locale.Tr "explore.relevant_repositories" ((printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))|Escape) | Safe}}</span> - </div> -{{end}} -<div class="divider"></div> diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl index dfede2ffcc..53742bf0d9 100644 --- a/templates/explore/repos.tmpl +++ b/templates/explore/repos.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content explore repositories"> {{template "explore/navbar" .}} <div class="ui container"> - {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} </div> diff --git a/templates/explore/search.tmpl b/templates/explore/search.tmpl index 74b80436dc..1d984a2e37 100644 --- a/templates/explore/search.tmpl +++ b/templates/explore/search.tmpl @@ -1,23 +1,22 @@ -<div class="ui secondary filter menu gt-ac gt-mx-0"> - <form class="ui form ignore-dirty gt-f1"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> - </div> +<div class="ui small secondary filter menu tw-items-center tw-mx-0"> + <form class="ui form ignore-dirty tw-flex-1"> + {{if .PageIsExploreUsers}} + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}} + {{else}} + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.org_kind")}} + {{end}} </form> <!-- Sort --> - <div class="ui dropdown type jump item gt-mr-0"> + <div class="ui small dropdown type jump item tw-mr-0"> <span class="text"> {{ctx.Locale.Tr "repo.issues.filter_sort"}} </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if eq .SortType "newest"}}active {{end}}item" href="{{$.Link}}?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> - <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if eq .SortType "newest"}}active {{end}}item" href="?sort=newest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?sort=oldest&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="?sort=alphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> + <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> </div> </div> </div> diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl index 9abbff6d9c..f2cf939ffb 100644 --- a/templates/explore/user_list.tmpl +++ b/templates/explore/user_list.tmpl @@ -1,6 +1,6 @@ <div class="flex-list"> {{range .Users}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> {{ctx.AvatarUtils.Avatar . 48}} </div> @@ -21,11 +21,13 @@ <a href="mailto:{{.Email}}">{{.Email}}</a> </span> {{end}} - <span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}</span> + <span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix)}}</span> </div> </div> </div> {{else}} - <div class="flex-item">{{ctx.Locale.Tr "explore.user_no_results"}}</div> + <div class="flex-item"> + {{ctx.Locale.Tr "search.no_results"}} + </div> {{end}} </div> diff --git a/templates/explore/users.tmpl b/templates/explore/users.tmpl index 7e15ae3d47..e9046125c6 100644 --- a/templates/explore/users.tmpl +++ b/templates/explore/users.tmpl @@ -3,9 +3,7 @@ {{template "explore/navbar" .}} <div class="ui container"> {{template "explore/search" .}} - {{template "explore/user_list" .}} - {{template "base/paginate" .}} </div> </div> diff --git a/templates/home.tmpl b/templates/home.tmpl index 78364431e9..e6fd4ef020 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -1,6 +1,6 @@ {{template "base/head" .}} <div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}" class="page-content home"> - <div class="gt-mb-5 gt-px-5"> + <div class="tw-mb-8 tw-px-8"> <div class="center"> <img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}"> <div class="hero"> @@ -17,7 +17,7 @@ {{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}} </h1> <p class="large"> - {{ctx.Locale.Tr "startpage.install_desc" | Str2html}} + {{ctx.Locale.Tr "startpage.install_desc"}} </p> </div> <div class="eight wide center column"> @@ -25,7 +25,7 @@ {{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}} </h1> <p class="large"> - {{ctx.Locale.Tr "startpage.platform_desc" | Str2html}} + {{ctx.Locale.Tr "startpage.platform_desc"}} </p> </div> </div> @@ -35,7 +35,7 @@ {{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}} </h1> <p class="large"> - {{ctx.Locale.Tr "startpage.lightweight_desc" | Str2html}} + {{ctx.Locale.Tr "startpage.lightweight_desc"}} </p> </div> <div class="eight wide center column"> @@ -43,7 +43,7 @@ {{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}} </h1> <p class="large"> - {{ctx.Locale.Tr "startpage.license_desc" | Str2html}} + {{ctx.Locale.Tr "startpage.license_desc"}} </p> </div> </div> diff --git a/templates/install.tmpl b/templates/install.tmpl index e9b267fa1c..8a6956b546 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -8,7 +8,7 @@ <div class="ui attached segment"> {{template "base/alert" .}} - <p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker" | Safe}}</p> + <p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p> <form class="ui form" action="{{AppSubUrl}}/" method="post"> <!-- Database Settings --> @@ -28,7 +28,7 @@ </div> </div> - <div class="gt-mt-4 gt-hidden" data-db-setting-for="common-host"> + <div class="tw-mt-4 tw-hidden" data-db-setting-for="common-host"> <div class="inline required field {{if .Err_DbSetting}}error{{end}}"> <label for="db_host">{{ctx.Locale.Tr "install.host"}}</label> <input id="db_host" name="db_host" value="{{.db_host}}"> @@ -47,7 +47,7 @@ </div> </div> - <div class="gt-mt-4 gt-hidden" data-db-setting-for="postgres"> + <div class="tw-mt-4 tw-hidden" data-db-setting-for="postgres"> <div class="inline required field"> <label>{{ctx.Locale.Tr "install.ssl_mode"}}</label> <div class="ui selection database type dropdown"> @@ -68,11 +68,11 @@ </div> </div> - <div class="gt-mt-4 gt-hidden" data-db-setting-for="sqlite3"> + <div class="tw-mt-4 tw-hidden" data-db-setting-for="sqlite3"> <div class="inline required field {{if or .Err_DbPath .Err_DbSetting}}error{{end}}"> <label for="db_path">{{ctx.Locale.Tr "install.path"}}</label> <input id="db_path" name="db_path" value="{{.db_path}}"> - <span class="help">{{ctx.Locale.Tr "install.sqlite_helper" | Safe}}</span> + <span class="help">{{ctx.Locale.Tr "install.sqlite_helper"}}</span> </div> </div> @@ -160,7 +160,7 @@ <!-- Email --> <details class="optional field"> - <summary class="right-content gt-py-3{{if .Err_SMTP}} text red{{end}}"> + <summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}"> {{ctx.Locale.Tr "install.email_title"}} </summary> <div class="inline field"> @@ -200,7 +200,7 @@ <!-- Server and other services --> <details class="optional field"> - <summary class="right-content gt-py-3{{if .Err_Services}} text red{{end}}"> + <summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}"> {{ctx.Locale.Tr "install.server_service_title"}} </summary> <div class="inline field"> @@ -298,7 +298,7 @@ <!-- Admin --> <details class="optional field"> - <summary class="right-content gt-py-3{{if .Err_Admin}} text red{{end}}"> + <summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}"> {{ctx.Locale.Tr "install.admin_title"}} </summary> <p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p> @@ -327,7 +327,7 @@ <div class="right-content"> {{ctx.Locale.Tr "install.env_config_keys_prompt"}} </div> - <div class="right-content gt-mt-3"> + <div class="right-content tw-mt-2"> {{range .EnvConfigKeys}}<span class="ui label">{{.}}</span>{{end}} </div> </div> @@ -338,7 +338,7 @@ <div class="right-content"> These configuration options will be written into: {{.CustomConfFile}} </div> - <div class="right-content gt-mt-3"> + <div class="right-content tw-mt-2"> <button class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button> </div> </div> @@ -347,5 +347,5 @@ </div> </div> </div> -<img class="gt-hidden" src="{{AssetUrlPrefix}}/img/loading.png"> +<img class="tw-hidden" src="{{AssetUrlPrefix}}/img/loading.png"> {{template "base/footer" .}} diff --git a/templates/mail/auth/activate.tmpl b/templates/mail/auth/activate.tmpl index a15afe3d49..b1bb4cb463 100644 --- a/templates/mail/auth/activate.tmpl +++ b/templates/mail/auth/activate.tmpl @@ -8,8 +8,8 @@ {{$activate_url := printf "%suser/activate?code=%s" AppUrl (QueryEscape .Code)}} <body> - <p>{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName | Str2html}}</p><br> - <p>{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives | Str2html}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br> + <p>{{.locale.Tr "mail.activate_account.text_1" (.DisplayName|DotEscape) AppName}}</p><br> + <p>{{.locale.Tr "mail.activate_account.text_2" .ActiveCodeLives}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br> <p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p> <p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p> diff --git a/templates/mail/auth/activate_email.tmpl b/templates/mail/auth/activate_email.tmpl index b15cc2a68a..3d32f80a4e 100644 --- a/templates/mail/auth/activate_email.tmpl +++ b/templates/mail/auth/activate_email.tmpl @@ -8,8 +8,8 @@ {{$activate_url := printf "%suser/activate_email?code=%s&email=%s" AppUrl (QueryEscape .Code) (QueryEscape .Email)}} <body> - <p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br> - <p>{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives | Str2html}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br> + <p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br> + <p>{{.locale.Tr "mail.activate_email.text" .ActiveCodeLives}}</p><p><a href="{{$activate_url}}">{{$activate_url}}</a></p><br> <p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p> <p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p> diff --git a/templates/mail/auth/register_notify.tmpl b/templates/mail/auth/register_notify.tmpl index 3cdb456fb3..62dbf7d927 100644 --- a/templates/mail/auth/register_notify.tmpl +++ b/templates/mail/auth/register_notify.tmpl @@ -8,10 +8,10 @@ {{$set_pwd_url := printf "%[1]suser/forgot_password" AppUrl}} <body> - <p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br> + <p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br> <p>{{.locale.Tr "mail.register_notify.text_1" AppName}}</p><br> <p>{{.locale.Tr "mail.register_notify.text_2" .Username}}</p><p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p><br> - <p>{{.locale.Tr "mail.register_notify.text_3" ($set_pwd_url | Escape) | Str2html}}</p><br> + <p>{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}</p><br> <p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p> </body> diff --git a/templates/mail/auth/reset_passwd.tmpl b/templates/mail/auth/reset_passwd.tmpl index 172844c954..55b1ecec3f 100644 --- a/templates/mail/auth/reset_passwd.tmpl +++ b/templates/mail/auth/reset_passwd.tmpl @@ -8,8 +8,8 @@ {{$recover_url := printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}} <body> - <p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape) | Str2html}}</p><br> - <p>{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives | Str2html}}</p><p><a href="{{$recover_url}}">{{$recover_url}}</a></p><br> + <p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br> + <p>{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}</p><p><a href="{{$recover_url}}">{{$recover_url}}</a></p><br> <p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p> <p>© <a target="_blank" rel="noopener noreferrer" href="{{AppUrl}}">{{AppName}}</a></p> diff --git a/templates/mail/issue/assigned.tmpl b/templates/mail/issue/assigned.tmpl index d02ea39918..5720319ee8 100644 --- a/templates/mail/issue/assigned.tmpl +++ b/templates/mail/issue/assigned.tmpl @@ -8,14 +8,14 @@ <title>{{.Subject}}</title> </head> -{{$repo_url := printf "<a href='%s'>%s</a>" (Escape .Issue.Repo.HTMLURL) (Escape .Issue.Repo.FullName)}} -{{$link := printf "<a href='%s'>#%d</a>" (Escape .Link) .Issue.Index}} +{{$repo_url := HTMLFormat "<a href='%s'>%s</a>" .Issue.Repo.HTMLURL .Issue.Repo.FullName}} +{{$link := HTMLFormat "<a href='%s'>#%d</a>" .Link .Issue.Index}} <body> <p> {{if .IsPull}} - {{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url | Str2html}} + {{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url}} {{else}} - {{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url | Str2html}} + {{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url}} {{end}} </p> <div class="footer"> diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl index 422a4f0461..395b118d3e 100644 --- a/templates/mail/issue/default.tmpl +++ b/templates/mail/issue/default.tmpl @@ -16,56 +16,56 @@ </head> <body> - {{if .IsMention}}<p>{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name | Str2html}}</p>{{end}} + {{if .IsMention}}<p>{{.locale.Tr "mail.issue.x_mentioned_you" .Doer.Name}}</p>{{end}} {{if eq .ActionName "push"}} <p> {{if .Comment.IsForcePush}} {{$oldCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.OldCommit}} {{$oldShortSha := ShortSha .Comment.OldCommit}} - {{$oldCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $oldCommitUrl) (Escape $oldShortSha)}} + {{$oldCommitLink := HTMLFormat "<a href='%[1]s'><b>%[2]s</b></a>" $oldCommitUrl $oldShortSha}} {{$newCommitUrl := printf "%s/commit/%s" .Comment.Issue.PullRequest.BaseRepo.HTMLURL .Comment.NewCommit}} {{$newShortSha := ShortSha .Comment.NewCommit}} - {{$newCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $newCommitUrl) (Escape $newShortSha)}} + {{$newCommitLink := HTMLFormat "<a href='%[1]s'><b>%[2]s</b></a>" $newCommitUrl $newShortSha}} - {{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink | Str2html}} + {{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink}} {{else}} - {{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}} + {{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits)}} {{end}} </p> {{end}} <p> {{if eq .ActionName "close"}} - {{.locale.Tr "mail.issue.action.close" (Escape .Doer.Name) .Issue.Index | Str2html}} + {{.locale.Tr "mail.issue.action.close" .Doer.Name .Issue.Index}} {{else if eq .ActionName "reopen"}} - {{.locale.Tr "mail.issue.action.reopen" (Escape .Doer.Name) .Issue.Index | Str2html}} + {{.locale.Tr "mail.issue.action.reopen" .Doer.Name .Issue.Index}} {{else if eq .ActionName "merge"}} - {{.locale.Tr "mail.issue.action.merge" (Escape .Doer.Name) .Issue.Index (Escape .Issue.PullRequest.BaseBranch) | Str2html}} + {{.locale.Tr "mail.issue.action.merge" .Doer.Name .Issue.Index .Issue.PullRequest.BaseBranch}} {{else if eq .ActionName "approve"}} - {{.locale.Tr "mail.issue.action.approve" (Escape .Doer.Name) | Str2html}} + {{.locale.Tr "mail.issue.action.approve" .Doer.Name}} {{else if eq .ActionName "reject"}} - {{.locale.Tr "mail.issue.action.reject" (Escape .Doer.Name) | Str2html}} + {{.locale.Tr "mail.issue.action.reject" .Doer.Name}} {{else if eq .ActionName "review"}} - {{.locale.Tr "mail.issue.action.review" (Escape .Doer.Name) | Str2html}} + {{.locale.Tr "mail.issue.action.review" .Doer.Name}} {{else if eq .ActionName "review_dismissed"}} - {{.locale.Tr "mail.issue.action.review_dismissed" (Escape .Doer.Name) (Escape .Comment.Review.Reviewer.Name) | Str2html}} + {{.locale.Tr "mail.issue.action.review_dismissed" .Doer.Name .Comment.Review.Reviewer.Name}} {{else if eq .ActionName "ready_for_review"}} - {{.locale.Tr "mail.issue.action.ready_for_review" (Escape .Doer.Name) | Str2html}} + {{.locale.Tr "mail.issue.action.ready_for_review" .Doer.Name}} {{end}} {{- if eq .Body ""}} {{if eq .ActionName "new"}} - {{.locale.Tr "mail.issue.action.new" (Escape .Doer.Name) .Issue.Index | Str2html}} + {{.locale.Tr "mail.issue.action.new" .Doer.Name .Issue.Index}} {{end}} {{else}} - {{.Body | Str2html}} + {{.Body}} {{end -}} {{- range .ReviewComments}} <hr> {{$.locale.Tr "mail.issue.in_tree_path" .TreePath}} <div class="review"> <pre>{{.Patch}}</pre> - <div>{{.RenderedContent | Safe}}</div> + <div>{{.RenderedContent}}</div> </div> {{end -}} {{if eq .ActionName "push"}} diff --git a/templates/mail/notify/repo_transfer.tmpl b/templates/mail/notify/repo_transfer.tmpl index 43d95b3ff0..8c8b276484 100644 --- a/templates/mail/notify/repo_transfer.tmpl +++ b/templates/mail/notify/repo_transfer.tmpl @@ -5,10 +5,10 @@ <title>{{.Subject}}</title> </head> -{{$url := printf "<a href='%[1]s'>%[2]s</a>" (Escape .Link) (Escape .Repo)}} +{{$url := HTMLFormat "<a href='%[1]s'>%[2]s</a>" .Link .Repo}} <body> <p>{{.Subject}}. - {{.locale.Tr "mail.repo.transfer.body" $url | Str2html}} + {{.locale.Tr "mail.repo.transfer.body" $url}} </p> <p> --- diff --git a/templates/mail/release.tmpl b/templates/mail/release.tmpl index f588d8224f..90a3caa4c5 100644 --- a/templates/mail/release.tmpl +++ b/templates/mail/release.tmpl @@ -11,18 +11,18 @@ </head> -{{$release_url := printf "<a href='%s'>%s</a>" (.Release.HTMLURL | Escape) (.Release.TagName | Escape)}} -{{$repo_url := printf "<a href='%s'>%s</a>" (.Release.Repo.HTMLURL | Escape) (.Release.Repo.FullName | Escape)}} +{{$release_url := HTMLFormat "<a href='%s'>%s</a>" .Release.HTMLURL .Release.TagName}} +{{$repo_url := HTMLFormat "<a href='%s'>%s</a>" .Release.Repo.HTMLURL .Release.Repo.FullName}} <body> <p> - {{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url | Str2html}} + {{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url}} </p> <h4>{{.locale.Tr "mail.release.title" .Release.Title}}</h4> <p> {{.locale.Tr "mail.release.note"}}<br> {{- if eq .Release.RenderedNote ""}} {{else}} - {{.Release.RenderedNote | Str2html}} + {{.Release.RenderedNote}} {{end -}} </p> <br><br> diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl index d21b7843ec..cb0c0c0a50 100644 --- a/templates/mail/team_invite.tmpl +++ b/templates/mail/team_invite.tmpl @@ -5,7 +5,7 @@ <meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no"> </head> <body> - <p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}</p> + <p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName)}}</p> <p>{{.locale.Tr "mail.team_invite.text_2"}}</p><p><a href="{{.InviteURL}}">{{.InviteURL}}</a></p> <p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p> <p>{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}</p> diff --git a/templates/org/follow_unfollow.tmpl b/templates/org/follow_unfollow.tmpl new file mode 100644 index 0000000000..ba0bd01efe --- /dev/null +++ b/templates/org/follow_unfollow.tmpl @@ -0,0 +1,7 @@ +<button class="ui basic button tw-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}"> + {{if $.IsFollowing}} + {{ctx.Locale.Tr "user.unfollow"}} + {{else}} + {{ctx.Locale.Tr "user.follow"}} + {{end}} +</button> diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 7b912c1c56..7361df99ea 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -1,18 +1,32 @@ -{{with .Org}} - <div class="ui container"> - <div class="ui vertically grid head"> - <div class="column"> - <div class="ui header gt-df gt-ac gt-word-break"> - {{ctx.AvatarUtils.Avatar . 100}} - <span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span> - <span class="org-visibility"> - {{if .Visibility.IsLimited}}<div class="ui medium basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</div>{{end}} - {{if .Visibility.IsPrivate}}<div class="ui medium basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</div>{{end}} - </span> - </div> - </div> +<div class="ui container tw-flex"> + {{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}} + <div id="org-info" class="tw-flex tw-flex-col"> + <div class="ui header"> + {{.Org.DisplayName}} + <span class="org-visibility"> + {{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}} + {{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}} + </span> + <span class="tw-flex tw-items-center tw-gap-1 tw-ml-auto tw-text-16 tw-whitespace-nowrap"> + {{if .EnableFeed}} + <a class="ui basic label button tw-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}"> + {{svg "octicon-rss" 24}} + </a> + {{end}} + {{if .IsSigned}} + {{template "org/follow_unfollow" .}} + {{end}} + </span> + </div> + {{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription}}</div>{{end}} + <div class="text light meta tw-mt-1"> + {{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}} + {{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}} + {{if .IsSigned}} + {{if .Org.Email}}<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>{{end}} + {{end}} </div> </div> -{{end}} +</div> {{template "org/menu" .}} diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index fc65d4691c..4851b69979 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -1,49 +1,14 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content organization profile"> - <div class="ui container gt-df"> - {{ctx.AvatarUtils.Avatar .Org 140 "org-avatar"}} - <div id="org-info"> - <div class="ui header"> - {{.Org.DisplayName}} - <span class="org-visibility"> - {{if .Org.Visibility.IsLimited}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}} - {{if .Org.Visibility.IsPrivate}}<span class="ui large basic horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}} - </span> - </div> - {{if $.RenderedDescription}}<div class="render-content markup">{{$.RenderedDescription|Str2html}}</div>{{end}} - <div class="text grey meta"> - {{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}} - {{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}} - {{if $.IsSigned}} - {{if .Org.Email}}<div class="flex-text-block">{{svg "octicon-mail"}} <a class="muted" href="mailto:{{.Org.Email}}">{{.Org.Email}}</a></div>{{end}} - {{end}} - </div> - </div> - <div class="right menu"> - {{if .EnableFeed}} - <a class="ui basic label button gt-mr-0" href="{{$.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}"> - {{svg "octicon-rss" 24}} - </a> - {{end}} - <button class="link-action ui basic button gt-mr-0" data-url="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}"> - {{if $.IsFollowing}} - {{ctx.Locale.Tr "user.unfollow"}} - {{else}} - {{ctx.Locale.Tr "user.follow"}} - {{end}} - </button> - </div> - </div> - - {{template "org/menu" .}} + {{template "org/header" .}} <div class="ui container"> <div class="ui mobile reversed stackable grid"> <div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column"> {{if .ProfileReadme}} - <div id="readme_profile" class="markup">{{.ProfileReadme | Str2html}}</div> + <div id="readme_profile" class="markup">{{.ProfileReadme}}</div> {{end}} - {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} </div> @@ -51,7 +16,7 @@ {{if .ShowMemberAndTeamTab}} <div class="ui five wide column"> {{if .CanCreateOrgRepo}} - <div class="center aligned"> + <div class="center aligned tw-mb-4"> <a class="ui primary button" href="{{AppSubUrl}}/repo/create?org={{.Org.ID}}">{{ctx.Locale.Tr "new_repo"}}</a> {{if not .DisableNewPullMirrors}} <a class="ui primary button" href="{{AppSubUrl}}/repo/migrate?org={{.Org.ID}}&mirror=1">{{ctx.Locale.Tr "new_migrate"}}</a> @@ -60,9 +25,9 @@ <div class="divider"></div> {{end}} {{if .NumMembers}} - <h4 class="ui top attached header gt-df"> - <strong class="gt-f1">{{ctx.Locale.Tr "org.members"}}</strong> - <a class="text grey gt-df gt-ac" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a> + <h4 class="ui top attached header tw-flex"> + <strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong> + <a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a> </h4> <div class="ui attached segment members"> {{$isMember := .IsOrganizationMember}} @@ -74,9 +39,9 @@ </div> {{end}} {{if .IsOrganizationMember}} - <div class="ui top attached header gt-df"> - <strong class="gt-f1">{{ctx.Locale.Tr "org.teams"}}</strong> - <a class="text grey gt-df gt-ac" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a> + <div class="ui top attached header tw-flex"> + <strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong> + <a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a> </div> <div class="ui attached table segment teams"> {{range .Teams}} diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index e4ddb69805..4388dc9520 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content organization"> +<div role="main" aria-label="{{.Title}}" class="page-content organization members"> {{template "org/header" .}} <div class="ui container"> {{template "base/alert" .}} @@ -7,7 +7,7 @@ <div class="flex-list"> {{range .Members}} {{$isPublic := index $.MembersIsPublicMember .ID}} - <div class="flex-item {{if $.PublicOnly}}gt-ac{{end}}"> + <div class="flex-item {{if $.PublicOnly}}tw-items-center{{end}}"> <div class="flex-item-leading"> <a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 48}}</a> </div> @@ -73,7 +73,7 @@ {{ctx.Locale.Tr "org.members.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p> + <p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> @@ -82,7 +82,7 @@ {{ctx.Locale.Tr "org.members.remove"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataOrganizationName"></span>` | Safe}}</p> + <p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 8a97711ce2..c519606d1f 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -1,50 +1,49 @@ <div class="ui container"> - <div class="ui secondary stackable pointing menu"> - <a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}"> - {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} - {{if .RepoCount}} - <div class="ui small label">{{.RepoCount}}</div> + <overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4"> + <div class="overflow-menu-items"> + <a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}"> + {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} + {{if .RepoCount}} + <div class="ui small label">{{.RepoCount}}</div> + {{end}} + </a> + {{if .CanReadProjects}} + <a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects"> + {{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}} + {{if .ProjectCount}} + <div class="ui small label">{{.ProjectCount}}</div> + {{end}} + </a> {{end}} - </a> - {{if .CanReadProjects}} - <a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects"> - {{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}} - {{if .ProjectCount}} - <div class="ui small label">{{.ProjectCount}}</div> + {{if and .IsPackageEnabled .CanReadPackages}} + <a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages"> + {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} + </a> {{end}} - </a> - {{end}} - {{if and .IsPackageEnabled .CanReadPackages}} - <a class="item" href="{{$.Org.HomeLink}}/-/packages"> - {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} - </a> - {{end}} - {{if and .IsRepoIndexerEnabled .CanReadCode}} - <a class="item" href="{{$.Org.HomeLink}}/-/code"> - {{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}} - </a> - {{end}} - {{if .NumMembers}} + {{if and .IsRepoIndexerEnabled .CanReadCode}} + <a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code"> + {{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}} + </a> + {{end}} + {{if .NumMembers}} <a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members"> - {{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}} + {{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}} <div class="ui small label">{{.NumMembers}}</div> </a> - {{end}} - {{if .IsOrganizationMember}} + {{end}} + {{if .IsOrganizationMember}} <a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams"> - {{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}} + {{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}} {{if .NumTeams}} <div class="ui small label">{{.NumTeams}}</div> {{end}} </a> - {{end}} - - {{if .IsOrganizationOwner}} - <div class="right menu"> - <a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings"> - {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} - </a> - </div> - {{end}} - </div> + {{end}} + {{if .IsOrganizationOwner}} + <a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings"> + {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} + </a> + {{end}} + </div> + </overflow-menu> </div> diff --git a/templates/org/projects/list.tmpl b/templates/org/projects/list.tmpl index 689091e5e0..80dde1c4d2 100644 --- a/templates/org/projects/list.tmpl +++ b/templates/org/projects/list.tmpl @@ -1,10 +1,9 @@ {{template "base/head" .}} {{if .ContextUser.IsOrganization}} - <div role="main" aria-label="{{.Title}}" class="page-content repository packages"> - {{template "shared/user/org_profile_avatar" .}} + <div role="main" aria-label="{{.Title}}" class="page-content organization projects"> + {{template "org/header" .}} <div class="ui container"> - {{template "user/overview/header" .}} - {{template "projects/list" .}} + {{template "projects/list" .}} </div> </div> {{else}} @@ -14,11 +13,9 @@ <div class="ui four wide column"> {{template "shared/user/profile_big_avatar" .}} </div> - <div class="ui twelve wide column"> - <div class="gt-mb-4"> + <div class="ui twelve wide column tw-mb-4"> {{template "user/overview/header" .}} - </div> - {{template "projects/list" .}} + {{template "projects/list" .}} </div> </div> </div> diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl index 495204b06d..e1ab81c4cd 100644 --- a/templates/org/projects/view.tmpl +++ b/templates/org/projects/view.tmpl @@ -1,7 +1,7 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project"> {{template "shared/user/org_profile_avatar" .}} - <div class="ui container"> + <div class="ui container tw-mb-4"> {{template "user/overview/header" .}} </div> <div class="ui container fluid padded"> diff --git a/templates/org/settings/blocked_users.tmpl b/templates/org/settings/blocked_users.tmpl new file mode 100644 index 0000000000..eab5ec0007 --- /dev/null +++ b/templates/org/settings/blocked_users.tmpl @@ -0,0 +1,5 @@ +{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked_users")}} +<div class="org-setting-content"> + {{template "shared/user/blocked_users" .}} +</div> +{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl index 2cf8238f57..e1ef471e34 100644 --- a/templates/org/settings/delete.tmpl +++ b/templates/org/settings/delete.tmpl @@ -6,7 +6,7 @@ </h4> <div class="ui attached error segment"> <div class="ui red message"> - <p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt" | Str2html}}</p> + <p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}</p> </div> <form class="ui form ignore-dirty" id="delete-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl index b12ea8d9f4..25a562c975 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -1,8 +1,8 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}} <div class="org-setting-content"> - <div class="gt-df gt-ac"> - <div class="gt-f1"> - {{ctx.Locale.Tr "org.settings.labels_desc" | Str2html}} + <div class="tw-flex tw-items-center"> + <div class="tw-flex-1"> + {{ctx.Locale.Tr "org.settings.labels_desc"}} </div> <button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button> </div> diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index 64ae20f0a3..ce792f667c 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -17,6 +17,9 @@ {{ctx.Locale.Tr "settings.applications"}} </a> {{end}} + <a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users"> + {{ctx.Locale.Tr "user.block.list"}} + </a> {{if .EnablePackages}} <a class="{{if .PageIsSettingsPackages}}active {{end}}item" href="{{.OrgLink}}/settings/packages"> {{ctx.Locale.Tr "packages.title"}} diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 31c0d85d89..62debfc0ae 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -8,7 +8,7 @@ {{.CsrfTokenHtml}} <div class="required field {{if .Err_Name}}error{{end}}"> <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}} - <span class="text red gt-hidden" id="org-name-change-prompt"> + <span class="text red tw-hidden" id="org-name-change-prompt"> <br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}} </span> </label> diff --git a/templates/org/team/invite.tmpl b/templates/org/team/invite.tmpl index e003d14757..1167828d14 100644 --- a/templates/org/team/invite.tmpl +++ b/templates/org/team/invite.tmpl @@ -7,7 +7,7 @@ {{ctx.AvatarUtils.Avatar .Organization 140}} </div> <div class="content"> - <div class="header">{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name | Str2html}}</div> + <div class="header">{{ctx.Locale.Tr "org.teams.invite.title" .Team.Name .Organization.Name}}</div> <div class="meta">{{ctx.Locale.Tr "org.teams.invite.by" .Inviter.Name}}</div> <div class="description">{{ctx.Locale.Tr "org.teams.invite.description"}}</div> </div> diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index da63d82967..5719328a27 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -9,12 +9,12 @@ {{template "org/team/navbar" .}} {{if .IsOrganizationOwner}} <div class="ui attached segment"> - <form class="ui form ignore-dirty gt-df gt-fw gt-gap-3" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post"> + <form class="ui form ignore-dirty tw-flex tw-flex-wrap tw-gap-2" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/add" method="post"> {{.CsrfTokenHtml}} <input type="hidden" name="uid" value="{{.SignedUser.ID}}"> - <div id="search-user-box" class="ui search gt-mr-3"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}> + <div id="search-user-box" class="ui search tw-mr-2"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}> <div class="ui input"> - <input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required> + <input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required> </div> </div> <button class="ui primary button">{{ctx.Locale.Tr "org.teams.add_team_member"}}</button> @@ -24,7 +24,7 @@ <div class="ui attached segment"> <div class="flex-list"> {{range .Team.Members}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> <a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a> </div> @@ -56,7 +56,7 @@ <div class="ui attached segment"> <div class="flex-list"> {{range .Invites}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-main"> {{.Email}} </div> @@ -81,7 +81,7 @@ {{ctx.Locale.Tr "org.members.remove"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.remove.detail" `<span class="name"></span>` `<span class="dataTeamName"></span>` | Safe}}</p> + <p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataTeamName"></span>`|SafeHTML)}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index 0178a20fbb..9608eac154 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -32,14 +32,14 @@ <div class="ui radio checkbox"> <input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}> <label>{{ctx.Locale.Tr "org.teams.specific_repositories"}}</label> - <span class="help">{{ctx.Locale.Tr "org.teams.specific_repositories_helper" | Str2html}}</span> + <span class="help">{{ctx.Locale.Tr "org.teams.specific_repositories_helper"}}</span> </div> </div> <div class="field"> <div class="ui radio checkbox"> <input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}> <label>{{ctx.Locale.Tr "org.teams.all_repositories"}}</label> - <span class="help">{{ctx.Locale.Tr "org.teams.all_repositories_helper" | Str2html}}</span> + <span class="help">{{ctx.Locale.Tr "org.teams.all_repositories_helper"}}</span> </div> </div> @@ -71,18 +71,18 @@ </div> <div class="divider"></div> - <div class="team-units required grouped field {{if eq .Team.AccessMode 3}}gt-hidden{{end}}"> + <div class="team-units required grouped field {{if eq .Team.AccessMode 3}}tw-hidden{{end}}"> <label>{{ctx.Locale.Tr "org.team_unit_desc"}}</label> <table class="ui celled table"> <thead> <tr> <th>{{ctx.Locale.Tr "units.unit"}}</th> <th class="center aligned">{{ctx.Locale.Tr "org.teams.none_access"}} - <span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th> + <span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th> <th class="center aligned">{{ctx.Locale.Tr "org.teams.read_access"}} - <span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th> + <span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th> <th class="center aligned">{{ctx.Locale.Tr "org.teams.write_access"}} - <span class="gt-vm" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "gt-ml-2"}}</span></th> + <span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th> </tr> </thead> <tbody> diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 5a32eea64f..98b4854eb8 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -9,17 +9,17 @@ {{template "org/team/navbar" .}} {{$canAddRemove := and $.IsOrganizationOwner (not $.Team.IncludesAllRepositories)}} {{if $canAddRemove}} - <div class="ui attached segment gt-df gt-fw gt-gap-3"> - <form class="ui form ignore-dirty gt-f1 gt-df" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post"> + <div class="ui attached segment tw-flex tw-flex-wrap tw-gap-2"> + <form class="ui form ignore-dirty tw-flex-1 tw-flex" action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/add" method="post"> {{.CsrfTokenHtml}} <div id="search-repo-box" data-uid="{{.Org.ID}}" class="ui search"> <div class="ui input"> - <input class="prompt" name="repo_name" placeholder="{{ctx.Locale.Tr "org.teams.search_repo_placeholder"}}" autocomplete="off" required> + <input class="prompt" name="repo_name" placeholder="{{ctx.Locale.Tr "search.repo_kind"}}" autocomplete="off" required> </div> </div> - <button class="ui primary button gt-ml-3">{{ctx.Locale.Tr "add"}}</button> + <button class="ui primary button tw-ml-2">{{ctx.Locale.Tr "add"}}</button> </form> - <div class="gt-dib"> + <div class="tw-inline-block"> <button class="ui primary button link-action" data-modal-confirm="{{ctx.Locale.Tr "org.teams.add_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/addall">{{ctx.Locale.Tr "add_all"}}</button> <button class="ui red button link-action" data-modal-confirm="{{ctx.Locale.Tr "org.teams.remove_all_repos_desc"}}" data-url="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/repo/removeall">{{ctx.Locale.Tr "remove_all"}}</button> </div> @@ -28,7 +28,7 @@ <div class="ui attached segment"> <div class="flex-list"> {{range .Team.Repos}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> {{template "repo/icon" .}} </div> diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 29e7cf7cdd..9311a46e38 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -27,16 +27,16 @@ </div> {{if eq .Team.LowerName "owners"}} <div class="item"> - {{ctx.Locale.Tr "org.teams.owners_permission_desc" | Str2html}} + {{ctx.Locale.Tr "org.teams.owners_permission_desc"}} </div> {{else}} <div class="item"> <h3>{{ctx.Locale.Tr "org.team_access_desc"}}</h3> <ul> {{if .Team.IncludesAllRepositories}} - <li>{{ctx.Locale.Tr "org.teams.all_repositories" | Str2html}}</li> + <li>{{ctx.Locale.Tr "org.teams.all_repositories"}}</li> {{else}} - <li>{{ctx.Locale.Tr "org.teams.specific_repositories" | Str2html}}</li> + <li>{{ctx.Locale.Tr "org.teams.specific_repositories"}}</li> {{end}} {{if .Team.CanCreateOrgRepo}} <li>{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</li> @@ -44,10 +44,10 @@ </ul> {{if (eq .Team.AccessMode 2)}} <h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3> - {{ctx.Locale.Tr "org.teams.write_permission_desc" | Str2html}} + {{ctx.Locale.Tr "org.teams.write_permission_desc"}} {{else if (eq .Team.AccessMode 3)}} <h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3> - {{ctx.Locale.Tr "org.teams.admin_permission_desc" | Str2html}} + {{ctx.Locale.Tr "org.teams.admin_permission_desc"}} {{else}} <table class="ui table"> <thead> @@ -88,7 +88,7 @@ {{ctx.Locale.Tr "org.teams.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p> + <p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index f4ceada2a7..53c909ee9c 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -49,7 +49,7 @@ {{ctx.Locale.Tr "org.teams.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.teams.leave.detail" `<span class="name"></span>` | Safe}}</p> + <p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/package/content/alpine.tmpl b/templates/package/content/alpine.tmpl index a1003cd6ff..5c144b9779 100644 --- a/templates/package/content/alpine.tmpl +++ b/templates/package/content/alpine.tmpl @@ -3,13 +3,13 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry" | Safe}}</label> - <div class="markup"><pre class="code-block"><code><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></gitea-origin-url>/$branch/$repository</code></pre></div> - <p>{{ctx.Locale.Tr "packages.alpine.registry.info" | Safe}}</p> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry"}}</label> + <div class="markup"><pre class="code-block"><code><origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></origin-url>/$branch/$repository</code></pre></div> + <p>{{ctx.Locale.Tr "packages.alpine.registry.info"}}</p> </div> <div class="field"> - <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key" | Safe}}</label> - <div class="markup"><pre class="code-block"><code>curl -JO <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></gitea-origin-url></code></pre></div> + <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key"}}</label> + <div class="markup"><pre class="code-block"><code>curl -JO <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></origin-url></code></pre></div> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.install"}}</label> @@ -18,7 +18,7 @@ </div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Alpine" "https://docs.gitea.com/usage/packages/alpine/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Alpine" "https://docs.gitea.com/usage/packages/alpine/"}}</label> </div> </div> </div> diff --git a/templates/package/content/cargo.tmpl b/templates/package/content/cargo.tmpl index 4dd7c3f731..7fd88a284a 100644 --- a/templates/package/content/cargo.tmpl +++ b/templates/package/content/cargo.tmpl @@ -3,13 +3,13 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cargo.registry" | Safe}}</label> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cargo.registry"}}</label> <div class="markup"><pre class="code-block"><code>[registry] default = "gitea" [registries.gitea] -index = "sparse+<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></gitea-origin-url>" # Sparse index -# index = "<gitea-origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></gitea-origin-url>" # Git +index = "sparse+<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></origin-url>" # Sparse index +# index = "<origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></origin-url>" # Git [net] git-fetch-with-cli = true</code></pre></div> @@ -19,7 +19,7 @@ git-fetch-with-cli = true</code></pre></div> <div class="markup"><pre class="code-block"><code>cargo add {{.PackageDescriptor.Package.Name}}@{{.PackageDescriptor.Version.Version}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/"}}</label> </div> </div> </div> diff --git a/templates/package/content/chef.tmpl b/templates/package/content/chef.tmpl index 0588c6e4b3..03ce9f852b 100644 --- a/templates/package/content/chef.tmpl +++ b/templates/package/content/chef.tmpl @@ -3,15 +3,15 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry" | Safe}}</label> - <div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></gitea-origin-url>'</code></pre></div> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry"}}</label> + <div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></origin-url>'</code></pre></div> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.chef.install"}}</label> <div class="markup"><pre class="code-block"><code>knife supermarket install {{.PackageDescriptor.Package.Name}} {{.PackageDescriptor.Version.Version}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/"}}</label> </div> </div> </div> diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl index 862f1c6925..c2dc6345c3 100644 --- a/templates/package/content/composer.tmpl +++ b/templates/package/content/composer.tmpl @@ -3,11 +3,11 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.composer.registry" | Safe}}</label> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.composer.registry"}}</label> <div class="markup"><pre class="code-block"><code>{ "repositories": [{ "type": "composer", - "url": "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></gitea-origin-url>" + "url": "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></origin-url>" } ] }</code></pre></div> @@ -17,7 +17,7 @@ <div class="markup"><pre class="code-block"><code>composer require {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Composer" "https://docs.gitea.com/usage/packages/composer/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Composer" "https://docs.gitea.com/usage/packages/composer/"}}</label> </div> </div> </div> diff --git a/templates/package/content/conan.tmpl b/templates/package/content/conan.tmpl index 55b84d12b1..b68a45fde3 100644 --- a/templates/package/content/conan.tmpl +++ b/templates/package/content/conan.tmpl @@ -4,14 +4,14 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.registry"}}</label> - <div class="markup"><pre class="code-block"><code>conan remote add gitea <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></gitea-origin-url></code></pre></div> + <div class="markup"><pre class="code-block"><code>conan remote add gitea <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></origin-url></code></pre></div> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.install"}}</label> <div class="markup"><pre class="code-block"><code>conan install --remote=gitea {{.PackageDescriptor.Package.Name}}/{{.PackageDescriptor.Version.Version}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Conan" "https://docs.gitea.com/usage/packages/conan/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Conan" "https://docs.gitea.com/usage/packages/conan/"}}</label> </div> </div> </div> diff --git a/templates/package/content/conda.tmpl b/templates/package/content/conda.tmpl index 0fd0c3db3f..031b51aa10 100644 --- a/templates/package/content/conda.tmpl +++ b/templates/package/content/conda.tmpl @@ -3,12 +3,12 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry" | Safe}}</label> - <div class="markup"><pre class="code-block"><code>channel_alias: <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry"}}</label> + <div class="markup"><pre class="code-block"><code>channel_alias: <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url> channels: -  - <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url> +  - <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url> default_channels: -  - <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url></code></pre></div> +  - <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url></code></pre></div> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conda.install"}}</label> @@ -16,7 +16,7 @@ default_channels: <div class="markup"><pre class="code-block"><code>conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Conda" "https://docs.gitea.com/usage/packages/conda/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Conda" "https://docs.gitea.com/usage/packages/conda/"}}</label> </div> </div> </div> diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index f5ee902c94..fe393f4388 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -19,7 +19,7 @@ <div class="markup"><pre class="code-block"><code>{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/"}}</label> </div> </div> </div> diff --git a/templates/package/content/cran.tmpl b/templates/package/content/cran.tmpl index f9a3f70107..ae58e6f334 100644 --- a/templates/package/content/cran.tmpl +++ b/templates/package/content/cran.tmpl @@ -3,15 +3,15 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry" | Safe}}</label> - <div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></gitea-origin-url>")))</code></pre></div> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry"}}</label> + <div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(gitea="<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></origin-url>")))</code></pre></div> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.cran.install"}}</label> <div class="markup"><pre class="code-block"><code>install.packages("{{.PackageDescriptor.Package.Name}}")</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "CRAN" "https://docs.gitea.com/usage/packages/cran/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "CRAN" "https://docs.gitea.com/usage/packages/cran/"}}</label> </div> </div> </div> diff --git a/templates/package/content/debian.tmpl b/templates/package/content/debian.tmpl index 1fde87f329..73b8257835 100644 --- a/templates/package/content/debian.tmpl +++ b/templates/package/content/debian.tmpl @@ -4,10 +4,10 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.registry"}}</label> - <div class="markup"><pre class="code-block"><code>sudo curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></gitea-origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc -echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></gitea-origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list + <div class="markup"><pre class="code-block"><code>sudo curl <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></origin-url> -o /etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc +echo "deb [signed-by=/etc/apt/keyrings/gitea-{{$.PackageDescriptor.Owner.Name}}.asc] <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/gitea.list sudo apt update</code></pre></div> - <p>{{ctx.Locale.Tr "packages.debian.registry.info" | Safe}}</p> + <p>{{ctx.Locale.Tr "packages.debian.registry.info"}}</p> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.install"}}</label> @@ -16,7 +16,7 @@ sudo apt update</code></pre></div> </div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Debian" "https://docs.gitea.com/usage/packages/debian/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Debian" "https://docs.gitea.com/usage/packages/debian/"}}</label> </div> </div> </div> diff --git a/templates/package/content/generic.tmpl b/templates/package/content/generic.tmpl index 05aa4aecad..2fd952105f 100644 --- a/templates/package/content/generic.tmpl +++ b/templates/package/content/generic.tmpl @@ -6,12 +6,12 @@ <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.generic.download"}}</label> <div class="markup"><pre class="code-block"><code> {{- range .PackageDescriptor.Files -}} -curl -OJ <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></gitea-origin-url> +curl -OJ <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></origin-url> {{end -}} </code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Generic" "https://docs.gitea.com/usage/packages/generic" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Generic" "https://docs.gitea.com/usage/packages/generic"}}</label> </div> </div> </div> diff --git a/templates/package/content/go.tmpl b/templates/package/content/go.tmpl index f98fc69fb6..80d1ab231a 100644 --- a/templates/package/content/go.tmpl +++ b/templates/package/content/go.tmpl @@ -4,10 +4,10 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.go.install"}}</label> - <div class="markup"><pre class="code-block"><code>GOPROXY=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></gitea-origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div> + <div class="markup"><pre class="code-block"><code>GOPROXY=<origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://docs.gitea.com/usage/packages/go"}}</label> </div> </div> </div> diff --git a/templates/package/content/helm.tmpl b/templates/package/content/helm.tmpl index 68e53133f1..da846e934d 100644 --- a/templates/package/content/helm.tmpl +++ b/templates/package/content/helm.tmpl @@ -4,7 +4,7 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.helm.registry"}}</label> - <div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></gitea-origin-url> + <div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></origin-url> helm repo update</code></pre></div> </div> <div class="field"> @@ -12,7 +12,7 @@ helm repo update</code></pre></div> <div class="markup"><pre class="code-block"><code>helm install {{.PackageDescriptor.Package.Name}} {{AppDomain}}/{{.PackageDescriptor.Package.Name}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Helm" "https://docs.gitea.com/usage/packages/helm/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Helm" "https://docs.gitea.com/usage/packages/helm/"}}</label> </div> </div> </div> diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl index b2cd567e16..3a7de335de 100644 --- a/templates/package/content/maven.tmpl +++ b/templates/package/content/maven.tmpl @@ -3,28 +3,28 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.registry" | Safe}}</label> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.registry"}}</label> <div class="markup"><pre class="code-block"><code><repositories> <repository> <id>gitea</id> - <url><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url></url> + <url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url> </repository> </repositories> <distributionManagement> <repository> <id>gitea</id> - <url><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url></url> + <url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url> </repository> <snapshotRepository> <id>gitea</id> - <url><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url></url> + <url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url> </snapshotRepository> </distributionManagement></code></pre></div> </div> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.install" | Safe}}</label> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.maven.install"}}</label> <div class="markup"><pre class="code-block"><code><dependency> <groupId>{{.PackageDescriptor.Metadata.GroupID}}</groupId> <artifactId>{{.PackageDescriptor.Metadata.ArtifactID}}</artifactId> @@ -37,10 +37,10 @@ </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.maven.download"}}</label> - <div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div> + <div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://docs.gitea.com/usage/packages/maven/"}}</label> </div> </div> </div> diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl index 882e999bed..a78a07d874 100644 --- a/templates/package/content/npm.tmpl +++ b/templates/package/content/npm.tmpl @@ -3,8 +3,8 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry" | Safe}}</label> - <div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></gitea-origin-url></code></pre></div> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.npm.registry"}}</label> + <div class="markup"><pre class="code-block"><code>{{if .PackageDescriptor.Metadata.Scope}}{{.PackageDescriptor.Metadata.Scope}}:{{end}}registry=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/npm/"></origin-url></code></pre></div> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.npm.install"}}</label> @@ -15,7 +15,7 @@ <div class="markup"><pre class="code-block"><code>"{{.PackageDescriptor.Package.Name}}": "{{.PackageDescriptor.Version.Version}}"</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "npm" "https://docs.gitea.com/usage/packages/npm/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "npm" "https://docs.gitea.com/usage/packages/npm/"}}</label> </div> </div> </div> diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl index 04dac89843..f1fe420c0b 100644 --- a/templates/package/content/nuget.tmpl +++ b/templates/package/content/nuget.tmpl @@ -4,24 +4,23 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.registry"}}</label> - <div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json"></gitea-origin-url></code></pre></div> + <div class="markup"><pre class="code-block"><code>dotnet nuget add source --name {{.PackageDescriptor.Owner.Name}} --username your_username --password your_token <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/nuget/index.json"></origin-url></code></pre></div> </div> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.nuget.install"}}</label> <div class="markup"><pre class="code-block"><code>dotnet add package --source {{.PackageDescriptor.Owner.Name}} --version {{.PackageDescriptor.Version.Version}} {{.PackageDescriptor.Package.Name}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "NuGet" "https://docs.gitea.com/usage/packages/nuget/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "NuGet" "https://docs.gitea.com/usage/packages/nuget/"}}</label> </div> </div> </div> - {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes}} + {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}} <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4> - <div class="ui attached segment"> - {{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}} - {{if .PackageDescriptor.Metadata.ReleaseNotes}}{{Str2html .PackageDescriptor.Metadata.ReleaseNotes}}{{end}} - </div> + {{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Description}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment markup markdown">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ReleaseNotes}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.ReleaseNotes}}</div>{{end}} {{end}} {{if .PackageDescriptor.Metadata.Dependencies}} diff --git a/templates/package/content/pub.tmpl b/templates/package/content/pub.tmpl index 8657d55dbf..f2c7ac938f 100644 --- a/templates/package/content/pub.tmpl +++ b/templates/package/content/pub.tmpl @@ -4,10 +4,10 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pub.install"}}</label> - <div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></gitea-origin-url></code></pre></div> + <div class="markup"><pre class="code-block"><code>dart pub add {{.PackageDescriptor.Package.Name}}:{{.PackageDescriptor.Version.Version}} --hosted-url=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pub/"></origin-url></code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Pub" "https://docs.gitea.com/usage/packages/pub/"}}</label> </div> </div> </div> diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl index ef9beb4280..817fced97b 100644 --- a/templates/package/content/pypi.tmpl +++ b/templates/package/content/pypi.tmpl @@ -4,10 +4,10 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label> - <div class="markup"><pre class="code-block"><code>pip install --index-url <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></gitea-origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div> + <div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label> </div> </div> </div> diff --git a/templates/package/content/rpm.tmpl b/templates/package/content/rpm.tmpl index 0f128fd3fb..3faa8a0dc7 100644 --- a/templates/package/content/rpm.tmpl +++ b/templates/package/content/rpm.tmpl @@ -11,13 +11,13 @@ # {{ctx.Locale.Tr "packages.rpm.distros.redhat"}} {{- range $group := .Groups}} {{- if $group}}{{$group = print "/" $group}}{{end}} -dnf config-manager --add-repo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url> +dnf config-manager --add-repo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url> {{- end}} # {{ctx.Locale.Tr "packages.rpm.distros.suse"}} {{- range $group := .Groups}} {{- if $group}}{{$group = print "/" $group}}{{end}} -zypper addrepo <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></gitea-origin-url> +zypper addrepo <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/rpm{{$group}}.repo"></origin-url> {{- end}}</code></pre></div> </div> <div class="field"> @@ -31,7 +31,7 @@ zypper install {{$.PackageDescriptor.Package.Name}}</code></pre> </div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "RPM" "https://docs.gitea.com/usage/packages/rpm/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "RPM" "https://docs.gitea.com/usage/packages/rpm/"}}</label> </div> </div> </div> diff --git a/templates/package/content/rubygems.tmpl b/templates/package/content/rubygems.tmpl index 180ff60f7d..610dfc7856 100644 --- a/templates/package/content/rubygems.tmpl +++ b/templates/package/content/rubygems.tmpl @@ -3,17 +3,17 @@ <div class="ui attached segment"> <div class="ui form"> <div class="field"> - <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install" | Safe}}:</label> - <div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version "{{.PackageDescriptor.Version.Version}}" --source "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></gitea-origin-url>"</code></pre></div> + <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.rubygems.install"}}:</label> + <div class="markup"><pre class="code-block"><code>gem install {{.PackageDescriptor.Package.Name}} --version "{{.PackageDescriptor.Version.Version}}" --source "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>"</code></pre></div> </div> <div class="field"> <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.rubygems.install2"}}:</label> - <div class="markup"><pre class="code-block"><code>source "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></gitea-origin-url>" do + <div class="markup"><pre class="code-block"><code>source "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/rubygems"></origin-url>" do gem "{{.PackageDescriptor.Package.Name}}", "{{.PackageDescriptor.Version.Version}}" end</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "RubyGems" "https://docs.gitea.com/usage/packages/rubygems/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "RubyGems" "https://docs.gitea.com/usage/packages/rubygems/"}}</label> </div> </div> </div> diff --git a/templates/package/content/swift.tmpl b/templates/package/content/swift.tmpl index ca36033df9..aacbc83980 100644 --- a/templates/package/content/swift.tmpl +++ b/templates/package/content/swift.tmpl @@ -4,10 +4,10 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.swift.registry"}}</label> - <div class="markup"><pre class="code-block"><code>swift package-registry set <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></gitea-origin-url></code></pre></div> + <div class="markup"><pre class="code-block"><code>swift package-registry set <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/swift"></origin-url></code></pre></div> </div> <div class="field"> - <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install" | Safe}}</label> + <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.swift.install"}}</label> <div class="markup"><pre class="code-block"><code>dependencies: [ .package(id: "{{.PackageDescriptor.Package.Name}}", from:"{{.PackageDescriptor.Version.Version}}") ]</code></pre></div> @@ -17,7 +17,7 @@ <div class="markup"><pre class="code-block"><code>swift package resolve</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Swift" "https://docs.gitea.com/usage/packages/swift/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Swift" "https://docs.gitea.com/usage/packages/swift/"}}</label> </div> </div> </div> diff --git a/templates/package/content/vagrant.tmpl b/templates/package/content/vagrant.tmpl index bbb461e4fb..7666284b87 100644 --- a/templates/package/content/vagrant.tmpl +++ b/templates/package/content/vagrant.tmpl @@ -4,10 +4,10 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.vagrant.install"}}</label> - <div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></gitea-origin-url>"</code></pre></div> + <div class="markup"><pre class="code-block"><code>vagrant box add --box-version {{.PackageDescriptor.Version.Version}} "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/vagrant/{{.PackageDescriptor.Package.Name}}"></origin-url>"</code></pre></div> </div> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Vagrant" "https://docs.gitea.com/usage/packages/vagrant/"}}</label> </div> </div> </div> diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl index 73cbc06aac..3e7f10f66a 100644 --- a/templates/package/metadata/alpine.tmpl +++ b/templates/package/metadata/alpine.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "alpine"}} {{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}} {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/cargo.tmpl b/templates/package/metadata/cargo.tmpl index c8471a71ef..5ad3c20a93 100644 --- a/templates/package/metadata/cargo.tmpl +++ b/templates/package/metadata/cargo.tmpl @@ -1,7 +1,7 @@ {{if eq .PackageDescriptor.Package.Type "cargo"}} - {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/chef.tmpl b/templates/package/metadata/chef.tmpl index fa6e068d23..23a9ce3ec0 100644 --- a/templates/package/metadata/chef.tmpl +++ b/templates/package/metadata/chef.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "chef"}} - {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/composer.tmpl b/templates/package/metadata/composer.tmpl index fbdc33f73d..0f6ff9d6f2 100644 --- a/templates/package/metadata/composer.tmpl +++ b/templates/package/metadata/composer.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "composer"}} - {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.Name}}</div>{{end}} - {{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}} + {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/conan.tmpl b/templates/package/metadata/conan.tmpl index 40bda555bb..4e05ec2587 100644 --- a/templates/package/metadata/conan.tmpl +++ b/templates/package/metadata/conan.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conan"}} - {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl index f70e2b2a1c..3628686e13 100644 --- a/templates/package/metadata/conda.tmpl +++ b/templates/package/metadata/conda.tmpl @@ -1,6 +1,6 @@ {{if eq .PackageDescriptor.Package.Type "conda"}} - {{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/metadata/container.tmpl b/templates/package/metadata/container.tmpl index b05ef0b846..f5abb7ef6e 100644 --- a/templates/package/metadata/container.tmpl +++ b/templates/package/metadata/container.tmpl @@ -1,9 +1,9 @@ {{if eq .PackageDescriptor.Package.Type "container"}} - <div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Type.Name}}</div> - {{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}} - {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}} - {{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} + <div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}</div> + {{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}} + {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl index 499f77e80d..50ea484999 100644 --- a/templates/package/metadata/helm.tmpl +++ b/templates/package/metadata/helm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "helm"}} - {{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.Name}}</div>{{end}} - {{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl index 36f5eca840..548be61790 100644 --- a/templates/package/metadata/maven.tmpl +++ b/templates/package/metadata/maven.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "maven"}} - {{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/npm.tmpl b/templates/package/metadata/npm.tmpl index 9794d851af..df37504e37 100644 --- a/templates/package/metadata/npm.tmpl +++ b/templates/package/metadata/npm.tmpl @@ -1,8 +1,8 @@ {{if eq .PackageDescriptor.Package.Type "npm"}} - {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} {{range .PackageDescriptor.VersionProperties}} - {{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "gt-mr-3"}} {{.Value}}</div>{{end}} + {{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}</div>{{end}} {{end}} {{end}} diff --git a/templates/package/metadata/nuget.tmpl b/templates/package/metadata/nuget.tmpl index f25e1c3b63..5534577bd2 100644 --- a/templates/package/metadata/nuget.tmpl +++ b/templates/package/metadata/nuget.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "nuget"}} - {{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/metadata/pub.tmpl b/templates/package/metadata/pub.tmpl index 1e4a90e78c..16f7cec370 100644 --- a/templates/package/metadata/pub.tmpl +++ b/templates/package/metadata/pub.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pub"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/metadata/pypi.tmpl b/templates/package/metadata/pypi.tmpl index f447cb7f4f..3d9b213907 100644 --- a/templates/package/metadata/pypi.tmpl +++ b/templates/package/metadata/pypi.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "pypi"}} - {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl index 026f129590..eda8a489f3 100644 --- a/templates/package/metadata/rpm.tmpl +++ b/templates/package/metadata/rpm.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "rpm"}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/rubygems.tmpl b/templates/package/metadata/rubygems.tmpl index 62150b1a43..9b11287691 100644 --- a/templates/package/metadata/rubygems.tmpl +++ b/templates/package/metadata/rubygems.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "rubygems"}} - {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div> {{end}} - {{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}</div>{{end}} + {{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div> {{end}} + {{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}} {{end}} diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl index 326ebe1a94..fdffb6dede 100644 --- a/templates/package/metadata/swift.tmpl +++ b/templates/package/metadata/swift.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "swift"}} {{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl index a92398a275..4628a2dcbb 100644 --- a/templates/package/metadata/vagrant.tmpl +++ b/templates/package/metadata/vagrant.tmpl @@ -1,5 +1,5 @@ {{if eq .PackageDescriptor.Package.Type "vagrant"}} - {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "gt-mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} - {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} + {{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}} {{end}} diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl index 6ef62753e2..9424baf493 100644 --- a/templates/package/settings.tmpl +++ b/templates/package/settings.tmpl @@ -1,10 +1,16 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content repository settings options"> - {{template "shared/user/org_profile_avatar" .}} +<div role="main" aria-label="{{.Title}}" class="page-content repository settings options{{if .ContextUser.IsOrganization}} organization{{end}}"> + {{if .ContextUser.IsOrganization}} + {{template "org/header" .}} + {{else}} + {{template "shared/user/org_profile_avatar" .}} + {{end}} <div class="ui container"> - {{template "user/overview/header" .}} + {{if not .ContextUser.IsOrganization}} + {{template "user/overview/header" .}} + {{end}} {{template "base/alert" .}} - <p><a href="{{.PackageDescriptor.FullWebLink}}">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p> + <p><a href="{{.PackageDescriptor.VersionWebLink}}">{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</a> / <strong>{{ctx.Locale.Tr "repo.settings"}}</strong></p> <h4 class="ui top attached header"> {{ctx.Locale.Tr "packages.settings.link"}} </h4> diff --git a/templates/package/shared/cargo.tmpl b/templates/package/shared/cargo.tmpl index b452065881..7652231465 100644 --- a/templates/package/shared/cargo.tmpl +++ b/templates/package/shared/cargo.tmpl @@ -18,7 +18,7 @@ <button class="ui primary button">{{ctx.Locale.Tr "packages.owner.settings.cargo.rebuild"}}</button> </form> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Cargo" "https://docs.gitea.com/usage/packages/cargo/"}}</label> </div> </div> </div> diff --git a/templates/package/shared/cleanup_rules/edit.tmpl b/templates/package/shared/cleanup_rules/edit.tmpl index 8729494412..138a90791c 100644 --- a/templates/package/shared/cleanup_rules/edit.tmpl +++ b/templates/package/shared/cleanup_rules/edit.tmpl @@ -40,7 +40,7 @@ <div class="field {{if .Err_KeepPattern}}error{{end}}"> <label>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</label> <input name="keep_pattern" type="text" value="{{.CleanupRule.KeepPattern}}"> - <p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container" | Safe}}</p> + <p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.keep.pattern.container"}}</p> </div> <div class="divider"></div> <p>{{ctx.Locale.Tr "packages.owner.settings.cleanuprules.remove.title"}}</p> diff --git a/templates/package/shared/cleanup_rules/preview.tmpl b/templates/package/shared/cleanup_rules/preview.tmpl index 7a50d5ccca..cff8e8249f 100644 --- a/templates/package/shared/cleanup_rules/preview.tmpl +++ b/templates/package/shared/cleanup_rules/preview.tmpl @@ -19,7 +19,7 @@ <tr> <td>{{.Package.Type.Name}}</td> <td>{{.Package.Name}}</td> - <td><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td> + <td><a href="{{.VersionWebLink}}">{{.Version.Version}}</a></td> <td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td> <td>{{FileSize .CalculateBlobSize}}</td> <td>{{DateTime "short" .Version.CreatedUnix}}</td> diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 740a96bb8d..e4e8eca91e 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -1,16 +1,16 @@ {{template "base/alert" .}} {{if .HasPackages}} <form class="ui form ignore-dirty"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Query}} - <select class="ui dropdown" name="type"> + <div class="ui small fluid action input"> + {{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}} + <select class="ui small dropdown" name="type"> <option value="">{{ctx.Locale.Tr "packages.filter.type"}}</option> <option value="all">{{ctx.Locale.Tr "packages.filter.type.all"}}</option> {{range $type := .AvailableTypes}} <option{{if eq $.PackageType $type}} selected="selected"{{end}} value="{{$type}}">{{$type.Name}}</option> {{end}} </select> - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> + {{template "shared/search/button"}} </div> </form> {{end}} @@ -20,7 +20,7 @@ <div class="flex-item"> <div class="flex-item-main"> <div class="flex-item-title"> - <a href="{{.FullWebLink}}">{{.Package.Name}}</a> + <a href="{{.VersionWebLink}}">{{.Package.Name}}</a> <span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span> </div> <div class="flex-item-body"> @@ -30,9 +30,9 @@ {{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}} {{end}} {{if $hasRepositoryAccess}} - {{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) .Repository.Link (.Repository.FullName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by_in" $timeStr .Creator.HomeLink .Creator.GetDisplayName .Repository.Link .Repository.FullName}} {{else}} - {{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by" $timeStr .Creator.HomeLink .Creator.GetDisplayName}} {{end}} </div> </div> @@ -45,12 +45,12 @@ <h2>{{ctx.Locale.Tr "packages.empty"}}</h2> {{if and .Repository .CanWritePackages}} {{$packagesUrl := URLJoin .Owner.HomeLink "-" "packages"}} - <p>{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl | Safe}}</p> + <p>{{ctx.Locale.Tr "packages.empty.repo" $packagesUrl}}</p> {{end}} - <p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/" | Safe}}</p> + <p>{{ctx.Locale.Tr "packages.empty.documentation" "https://docs.gitea.com/usage/packages/overview/"}}</p> </div> {{else}} - <p class="gt-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p> + <p class="tw-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p> {{end}} {{end}} {{template "base/paginate" .}} diff --git a/templates/package/shared/versionlist.tmpl b/templates/package/shared/versionlist.tmpl index fcf3030fe6..e5c568e059 100644 --- a/templates/package/shared/versionlist.tmpl +++ b/templates/package/shared/versionlist.tmpl @@ -1,21 +1,21 @@ <p><a href="{{.PackageDescriptor.PackageWebLink}}">{{.PackageDescriptor.Package.Name}}</a> / <strong>{{ctx.Locale.Tr "packages.versions"}}</strong></p> <form class="ui form ignore-dirty"> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Query}} - <select class="ui dropdown" name="sort"> + <div class="ui small fluid action input"> + {{template "shared/search/input" dict "Value" .Query "Placeholder" (ctx.Locale.Tr "search.package_kind")}} + <select class="ui small dropdown" name="sort"> <option value="version_asc"{{if eq .Sort "version_asc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "filter.string.asc"}}</option> <option value="version_desc"{{if eq .Sort "version_desc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "filter.string.desc"}}</option> <option value="created_asc"{{if eq .Sort "created_asc"}} selected="selected"{{end}}>{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</option> <option value="created_desc"{{if or (eq .Sort "") (eq .Sort "created_desc")}} selected="selected"{{end}}>{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</option> </select> {{if eq .PackageDescriptor.Package.Type "container"}} - <select class="ui dropdown" name="tagged"> + <select class="ui small dropdown" name="tagged"> {{$isTagged := or (eq .Tagged "") (eq .Tagged "tagged")}} <option value="tagged"{{if $isTagged}} selected="selected"{{end}}>{{ctx.Locale.Tr "packages.filter.container.tagged"}}</option> <option value="untagged"{{if not $isTagged}} selected="selected"{{end}}>{{ctx.Locale.Tr "packages.filter.container.untagged"}}</option> </select> {{end}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> + {{template "shared/search/button"}} </div> </form> <div> @@ -23,15 +23,15 @@ <div class="flex-list"> <div class="flex-item"> <div class="flex-item-main"> - <a class="flex-item-title" href="{{.FullWebLink}}">{{.Version.LowerVersion}}</a> + <a class="flex-item-title" href="{{.VersionWebLink}}">{{.Version.LowerVersion}}</a> <div class="flex-item-body"> - {{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix ctx.Locale) .Creator.HomeLink .Creator.GetDisplayName}} </div> </div> </div> </div> {{else}} - <p class="gt-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p> + <p class="tw-py-4">{{ctx.Locale.Tr "packages.filter.no_result"}}</p> {{end}} {{template "base/paginate" .}} </div> diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 553a46cfad..6beb249a7f 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -10,9 +10,9 @@ <div> {{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}} {{if .HasRepositoryAccess}} - {{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.Link (.PackageDescriptor.Repository.FullName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName .PackageDescriptor.Repository.Link .PackageDescriptor.Repository.FullName}} {{else}} - {{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr "packages.published_by" $timeStr .PackageDescriptor.Creator.HomeLink .PackageDescriptor.Creator.GetDisplayName}} {{end}} </div> </div> @@ -43,12 +43,12 @@ <div class="issue-content-right ui segment"> <strong>{{ctx.Locale.Tr "packages.details"}}</strong> <div class="ui relaxed list"> - <div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "gt-mr-3"}} {{.PackageDescriptor.Package.Type.Name}}</div> + <div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "tw-mr-2"}} {{.PackageDescriptor.Package.Type.Name}}</div> {{if .HasRepositoryAccess}} - <div class="item">{{svg "octicon-repo" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div> + <div class="item">{{svg "octicon-repo" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div> {{end}} - <div class="item">{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div> - <div class="item">{{svg "octicon-download" 16 "gt-mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}</div> + <div class="item">{{svg "octicon-calendar" 16 "tw-mr-2"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix ctx.Locale}}</div> + <div class="item">{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}</div> {{template "package/metadata/alpine" .}} {{template "package/metadata/cargo" .}} {{template "package/metadata/chef" .}} @@ -70,7 +70,7 @@ {{template "package/metadata/swift" .}} {{template "package/metadata/vagrant" .}} {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} - <div class="item">{{svg "octicon-database" 16 "gt-mr-3"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div> + <div class="item">{{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div> {{end}} </div> {{if not (eq .PackageDescriptor.Package.Type "container")}} @@ -87,11 +87,11 @@ {{end}} <div class="divider"></div> <strong>{{ctx.Locale.Tr "packages.versions"}} ({{.TotalVersionCount}})</strong> - <a class="gt-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a> + <a class="tw-float-right" href="{{$.PackageDescriptor.PackageWebLink}}/versions">{{ctx.Locale.Tr "packages.versions.view_all"}}</a> <div class="ui relaxed list"> {{range .LatestVersions}} - <div class="item gt-df"> - <a class="gt-f1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a> + <div class="item tw-flex"> + <a class="tw-flex-1 gt-ellipsis" title="{{.Version}}" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a> <span class="text small">{{DateTime "short" .CreatedUnix}}</span> </div> {{end}} @@ -100,10 +100,10 @@ <div class="divider"></div> <div class="ui relaxed list"> {{if .HasRepositoryAccess}} - <div class="item">{{svg "octicon-issue-opened" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div> + <div class="item">{{svg "octicon-issue-opened" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div> {{end}} {{if .CanWritePackages}} - <div class="item">{{svg "octicon-tools" 16 "gt-mr-3"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> + <div class="item">{{svg "octicon-tools" 16 "tw-mr-2"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> {{end}} </div> {{end}} diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index cbff82dd70..ec02e9a6fc 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -1,16 +1,16 @@ {{if and $.CanWriteProjects (not $.Repository.IsArchived)}} - <div class="gt-df gt-sb gt-mb-4"> + <div class="tw-flex tw-justify-between tw-mb-4"> <div class="small-menu-items ui compact tiny menu list-header-toggle"> - <a class="item{{if not .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=open&q={{$.Keyword}}"> - {{svg "octicon-project-symlink" 16 "gt-mr-3"}} + <a class="item{{if not .IsShowClosed}} active{{end}}" href="?state=open&q={{$.Keyword}}"> + {{svg "octicon-project-symlink" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} </a> - <a class="item{{if .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=closed&q={{$.Keyword}}"> - {{svg "octicon-check" 16 "gt-mr-3"}} + <a class="item{{if .IsShowClosed}} active{{end}}" href="?state=closed&q={{$.Keyword}}"> + {{svg "octicon-check" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} </a> </div> - <div class="gt-text-right"> + <div class="tw-text-right"> <a class="ui small primary button" href="{{$.Link}}/new">{{ctx.Locale.Tr "repo.projects.new"}}</a> </div> </div> @@ -21,13 +21,8 @@ <div class="list-header"> <!-- Search --> <form class="list-header-search ui form ignore-dirty"> - <div class="ui small search fluid action input"> - <input type="hidden" name="state" value="{{$.State}}"> - {{template "shared/searchinput" dict "Value" .Keyword}} - <button class="ui small icon button" type="submit" aria-label="{{ctx.Locale.Tr "explore.search"}}"> - {{svg "octicon-search"}} - </button> - </div> + <input type="hidden" name="state" value="{{$.State}}"> + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}} </form> <!-- Sort --> <div class="list-header-sort ui small dropdown type jump item"> @@ -36,9 +31,9 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> </div> </div> </div> @@ -46,7 +41,7 @@ <div class="milestone-list"> {{range .Projects}} <li class="milestone-card"> - <h3 class="flex-text-block gt-m-0"> + <h3 class="flex-text-block tw-m-0"> {{svg .IconName 16}} <a class="muted" href="{{.Link ctx}}">{{.Title}}</a> </h3> @@ -75,7 +70,7 @@ </div> {{if .Description}} <div class="content"> - {{.RenderedContent|Str2html}} + {{.RenderedContent}} </div> {{end}} </li> diff --git a/templates/projects/new.tmpl b/templates/projects/new.tmpl index 711dbe842a..92ee36c1c4 100644 --- a/templates/projects/new.tmpl +++ b/templates/projects/new.tmpl @@ -55,7 +55,7 @@ </div> </div> <div class="divider"></div> - <div class="gt-text-right"> + <div class="tw-text-right"> <a class="ui cancel button" href="{{$.CancelLink}}"> {{ctx.Locale.Tr "repo.milestones.cancel"}} </a> diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index b3ad03c354..f9b85360e0 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -1,8 +1,8 @@ {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}} <div class="ui container"> - <div class="gt-df gt-sb gt-ac gt-mb-4"> - <h2 class="gt-mb-0">{{.Project.Title}}</h2> + <div class="tw-flex tw-justify-between tw-items-center tw-mb-4"> + <h2 class="tw-mb-0">{{.Project.Title}}</h2> {{if $canWriteProject}} <div class="ui compact mini menu"> <a class="item" href="{{.Link}}/edit?redirect=project"> @@ -41,9 +41,9 @@ </div> <div class="field color-field"> - <label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label> - <div class="color picker column"> - <input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color"> + <label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label> + <div class="js-color-picker-input column"> + <input maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color"> {{template "repo/issue/label_precolors"}} </div> </div> @@ -58,7 +58,7 @@ {{end}} </div> - <div class="content">{{$.Project.RenderedContent|Str2html}}</div> + <div class="content">{{$.Project.RenderedContent}}</div> <div class="divider"></div> </div> @@ -66,17 +66,17 @@ <div id="project-board"> <div class="board {{if .CanWriteProjects}}sortable{{end}}"> {{range .Columns}} - <div class="ui segment project-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> - <div class="project-column-header"> - <div class="ui large label project-column-title gt-py-2"> + <div class="ui segment project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> + <div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}"> + <div class="ui large label project-column-title tw-py-1"> <div class="ui small circular grey label project-column-issue-count"> {{.NumIssues ctx}} </div> - {{.Title}} + <span class="project-column-title-label">{{.Title}}</span> </div> - {{if and $canWriteProject (ne .ID 0)}} + {{if $canWriteProject}} <div class="ui dropdown jump item"> - <div class="gt-px-3"> + <div class="tw-px-2"> {{svg "octicon-kebab-horizontal"}} </div> <div class="menu user-menu"> @@ -86,29 +86,20 @@ </a> {{if not .Default}} <a class="item show-modal button default-project-column-show" - data-modal="#default-project-column-modal-{{.ID}}" - data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" - data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" - data-url="{{$.Link}}/{{.ID}}/default"> + data-modal="#default-project-column-modal-{{.ID}}" + data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" + data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" + data-url="{{$.Link}}/{{.ID}}/default"> {{svg "octicon-pin"}} {{ctx.Locale.Tr "repo.projects.column.set_default"}} </a> - {{else}} - <a class="item show-modal button default-project-column-show" - data-modal="#default-project-column-modal-{{.ID}}" - data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.unset_default"}}" - data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.unset_default_desc"}}" - data-url="{{$.Link}}/{{.ID}}/unsetdefault"> - {{svg "octicon-pin-slash"}} - {{ctx.Locale.Tr "repo.projects.column.unset_default"}} + <a class="item show-modal button show-delete-project-column-modal" + data-modal="#delete-project-column-modal-{{.ID}}" + data-url="{{$.Link}}/{{.ID}}"> + {{svg "octicon-trash"}} + {{ctx.Locale.Tr "repo.projects.column.delete"}} </a> {{end}} - <a class="item show-modal button show-delete-project-column-modal" - data-modal="#delete-project-column-modal-{{.ID}}" - data-url="{{$.Link}}/{{.ID}}"> - {{svg "octicon-trash"}} - {{ctx.Locale.Tr "repo.projects.column.delete"}} - </a> <div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}"> <div class="header"> @@ -123,8 +114,8 @@ <div class="field color-field"> <label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label> - <div class="color picker column"> - <input class="color-picker" maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}"> + <div class="js-color-picker-input column"> + <input maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}"> {{template "repo/issue/label_precolors"}} </div> </div> @@ -162,12 +153,10 @@ </div> {{end}} </div> - - <div class="divider"></div> - - <div class="ui cards {{if and $canWriteProject (ne .ID 0)}}{{/* ID 0 is default column which cannot be moved */}}gt-cursor-grab{{end}}" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> + <div class="divider"{{if .Color}} style="color: {{ContrastColor .Color}} !important"{{end}}></div> + <div class="ui cards" data-url="{{$.Link}}/{{.ID}}" data-project="{{$.Project.ID}}" data-board="{{.ID}}" id="board_{{.ID}}"> {{range (index $.IssuesMap .ID)}} - <div class="issue-card gt-word-break {{if $canWriteProject}}gt-cursor-grab{{end}}" data-issue="{{.ID}}"> + <div class="issue-card gt-word-break {{if $canWriteProject}}tw-cursor-grab{{end}}" data-issue="{{.ID}}"> {{template "repo/issue/card" (dict "Issue" . "Page" $)}} </div> {{end}} diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl index 62d30305b3..b66d0e360a 100644 --- a/templates/repo/actions/list.tmpl +++ b/templates/repo/actions/list.tmpl @@ -8,9 +8,9 @@ <div class="ui stackable grid"> <div class="four wide column"> <div class="ui fluid vertical menu"> - <a class="item{{if not $.CurWorkflow}} active{{end}}" href="{{$.Link}}?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a> + <a class="item{{if not $.CurWorkflow}} active{{end}}" href="?actor={{$.CurActor}}&status={{$.CurStatus}}">{{ctx.Locale.Tr "actions.runs.all_workflows"}}</a> {{range .workflows}} - <a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="{{$.Link}}?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}} + <a class="item{{if eq .Entry.Name $.CurWorkflow}} active{{end}}" href="?workflow={{.Entry.Name}}&actor={{$.CurActor}}&status={{$.CurStatus}}">{{.Entry.Name}} {{if .ErrMsg}} <span data-tooltip-content="{{.ErrMsg}}"> {{svg "octicon-alert" 16 "text red"}} @@ -25,7 +25,7 @@ </div> </div> <div class="twelve wide column content"> - <div class="ui secondary filter menu gt-je gt-df gt-ac"> + <div class="ui secondary filter menu tw-justify-end tw-flex tw-items-center"> <!-- Actor --> <div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item"> <span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span> @@ -35,11 +35,11 @@ <i class="icon">{{svg "octicon-search"}}</i> <input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.actor"}}"> </div> - <a class="item{{if not $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0"> + <a class="item{{if not $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0"> {{ctx.Locale.Tr "actions.runs.actors_no_select"}} </a> {{range .Actors}} - <a class="item{{if eq .ID $.CurActor}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}"> + <a class="item{{if eq .ID $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}"> {{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}} </a> {{end}} @@ -54,11 +54,11 @@ <i class="icon">{{svg "octicon-search"}}</i> <input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.status"}}"> </div> - <a class="item{{if not $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0"> + <a class="item{{if not $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0"> {{ctx.Locale.Tr "actions.runs.status_no_select"}} </a> {{range .StatusInfoList}} - <a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="{{$.Link}}?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}"> + <a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}"> {{.DisplayedStatus}} </a> {{end}} @@ -66,7 +66,7 @@ </div> {{if .AllowDisableOrEnableWorkflow}} - <button class="ui jump dropdown btn interact-bg gt-p-3"> + <button class="ui jump dropdown btn interact-bg tw-p-2"> {{svg "octicon-kebab-horizontal"}} <div class="menu"> <a class="item link-action" data-url="{{$.Link}}/{{if .CurWorkflowDisabled}}enable{{else}}disable{{end}}?workflow={{$.CurWorkflow}}&actor={{.CurActor}}&status={{$.CurStatus}}"> diff --git a/templates/repo/actions/no_workflows.tmpl b/templates/repo/actions/no_workflows.tmpl index af1f28e8cf..009313581e 100644 --- a/templates/repo/actions/no_workflows.tmpl +++ b/templates/repo/actions/no_workflows.tmpl @@ -2,7 +2,7 @@ {{svg "octicon-no-entry" 48}} <h2>{{ctx.Locale.Tr "actions.runs.no_workflows"}}</h2> {{if and .CanWriteCode .CanWriteActions}} - <p>{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/" | Safe}}</p> + <p>{{ctx.Locale.Tr "actions.runs.no_workflows.quick_start" "https://docs.gitea.com/usage/actions/quickstart/"}}</p> {{end}} - <p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/" | Safe}}</p> + <p>{{ctx.Locale.Tr "actions.runs.no_workflows.documentation" "https://docs.gitea.com/usage/actions/overview/"}}</p> </div> diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index 580fb08a9e..ac5049cf56 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -6,7 +6,7 @@ </div> {{end}} {{range .Runs}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> {{template "repo/actions/status" (dict "status" .Status.String)}} </div> @@ -28,9 +28,9 @@ </div> <div class="flex-item-trailing"> {{if .RefLink}} - <a class="ui label gt-px-2 gt-mx-0" href="{{.RefLink}}">{{.PrettyRef}}</a> + <a class="ui label tw-px-1 tw-mx-0" href="{{.RefLink}}">{{.PrettyRef}}</a> {{else}} - <span class="ui label gt-px-2 gt-mx-0">{{.PrettyRef}}</span> + <span class="ui label tw-px-1 tw-mx-0">{{.PrettyRef}}</span> {{end}} </div> <div class="run-list-item-right"> diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl index 5016570142..a0e02cf8d7 100644 --- a/templates/repo/actions/status.tmpl +++ b/templates/repo/actions/status.tmpl @@ -12,7 +12,7 @@ {{- $className = .className -}} {{- end -}} -<span class="gt-df gt-ac" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}"> +<span class="tw-flex tw-items-center" data-tooltip-content="{{ctx.Locale.Tr (printf "actions.status.%s" .status)}}"> {{if eq .status "success"}} {{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}} {{else if eq .status "skipped"}} diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl index 6b07e7000a..f8b106147b 100644 --- a/templates/repo/actions/view.tmpl +++ b/templates/repo/actions/view.tmpl @@ -19,6 +19,7 @@ data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}" data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}" data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}" + data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}" data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}" data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}" data-locale-show-full-screen="{{ctx.Locale.Tr "show_full_screen"}}" diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl index 3149f20670..a19fb66261 100644 --- a/templates/repo/activity.tmpl +++ b/templates/repo/activity.tmpl @@ -1,235 +1,17 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository commits"> {{template "repo/header" .}} - <div class="ui container"> - <h2 class="ui header activity-header"> - <span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span> - <!-- Period --> - <div class="ui floating dropdown jump filter"> - <div class="ui basic compact button"> - {{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} - </div> - <div class="menu"> - <a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a> - <a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a> - <a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a> - <a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a> - <a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a> - <a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a> - <a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a> - </div> - </div> - </h2> - <div class="divider"></div> - - {{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}} - <h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4> - <div class="ui attached segment two column grid"> - {{if .Permission.CanRead $.UnitTypePullRequests}} - <div class="column"> - {{if gt .Activity.ActivePRCount 0}} - <div class="stats-table"> - <a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a> - <a href="#proposed-pull-requests" class="table-cell tiny background green"></a> - </div> - {{else}} - <div class="stats-table"> - <a class="table-cell tiny background light grey"></a> - </div> - {{end}} - {{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}} - </div> - {{end}} - {{if .Permission.CanRead $.UnitTypeIssues}} - <div class="column"> - {{if gt .Activity.ActiveIssueCount 0}} - <div class="stats-table"> - <a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a> - <a href="#new-issues" class="table-cell tiny background green"></a> - </div> - {{else}} - <div class="stats-table"> - <a class="table-cell tiny background light grey"></a> - </div> - {{end}} - {{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}} - </div> - {{end}} + <div class="ui container flex-container"> + <div class="flex-container-nav"> + {{template "repo/navbar" .}} </div> - <div class="ui attached segment horizontal segments"> - {{if .Permission.CanRead $.UnitTypePullRequests}} - <a href="#merged-pull-requests" class="ui attached segment text center"> - <span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br> - {{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}} - </a> - <a href="#proposed-pull-requests" class="ui attached segment text center"> - <span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br> - {{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}} - </a> - {{end}} - {{if .Permission.CanRead $.UnitTypeIssues}} - <a href="#closed-issues" class="ui attached segment text center"> - <span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br> - {{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}} - </a> - <a href="#new-issues" class="ui attached segment text center"> - <span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br> - {{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}} - </a> - {{end}} + <div class="flex-container-main"> + {{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}} + {{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}} + {{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}} + {{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}} </div> - {{end}} - - {{if .Permission.CanRead $.UnitTypeCode}} - {{if eq .Activity.Code.CommitCountInAllBranches 0}} - <div class="ui center aligned segment"> - <h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4> - </div> - {{end}} - {{if gt .Activity.Code.CommitCountInAllBranches 0}} - <div class="ui attached segment horizontal segments"> - <div class="ui attached segment text"> - {{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}} - <strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong> - {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}} - <strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong> - {{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}} - <strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong> - {{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}} - {{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}} - <strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong> - {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}} - {{ctx.Locale.Tr "repo.activity.git_stats_additions"}} - <strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong> - {{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}} - <strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>. - </div> - <div class="ui attached segment"> - <div id="repo-activity-top-authors-chart"></div> - </div> - </div> - {{end}} - {{end}} - - {{if gt .Activity.PublishedReleaseCount 0}} - <h4 class="divider divider-text gt-normal-case" id="published-releases"> - {{svg "octicon-tag" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.releases_published_by" - (ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount) - (ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount) - }} - </h4> - <div class="list"> - {{range .Activity.PublishedReleases}} - <p class="desc"> - <span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span> - {{.TagName}} - {{if not .IsTag}} - <a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> - {{end}} - {{TimeSinceUnix .CreatedUnix ctx.Locale}} - </p> - {{end}} - </div> - {{end}} - - {{if gt .Activity.MergedPRCount 0}} - <h4 class="divider divider-text gt-normal-case" id="merged-pull-requests"> - {{svg "octicon-git-pull-request" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.prs_merged_by" - (ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount) - (ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount) - }} - </h4> - <div class="list"> - {{range .Activity.MergedPRs}} - <p class="desc"> - <span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span> - #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> - {{TimeSinceUnix .MergedUnix ctx.Locale}} - </p> - {{end}} - </div> - {{end}} - - {{if gt .Activity.OpenedPRCount 0}} - <h4 class="divider divider-text gt-normal-case" id="proposed-pull-requests"> - {{svg "octicon-git-branch" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.prs_opened_by" - (ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount) - (ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount) - }} - </h4> - <div class="list"> - {{range .Activity.OpenedPRs}} - <p class="desc"> - <span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span> - #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> - {{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}} - </p> - {{end}} - </div> - {{end}} - - {{if gt .Activity.ClosedIssueCount 0}} - <h4 class="divider divider-text gt-normal-case" id="closed-issues"> - {{svg "octicon-issue-closed" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.issues_closed_from" - (ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount) - (ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount) - }} - </h4> - <div class="list"> - {{range .Activity.ClosedIssues}} - <p class="desc"> - <span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span> - #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> - {{TimeSinceUnix .ClosedUnix ctx.Locale}} - </p> - {{end}} - </div> - {{end}} - - {{if gt .Activity.OpenedIssueCount 0}} - <h4 class="divider divider-text gt-normal-case" id="new-issues"> - {{svg "octicon-issue-opened" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.activity.title.issues_created_by" - (ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount) - (ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount) - }} - </h4> - <div class="list"> - {{range .Activity.OpenedIssues}} - <p class="desc"> - <span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span> - #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> - {{TimeSinceUnix .CreatedUnix ctx.Locale}} - </p> - {{end}} - </div> - {{end}} - - {{if gt .Activity.UnresolvedIssueCount 0}} - <h4 class="divider divider-text gt-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}"> - {{svg "octicon-comment-discussion" 16 "gt-mr-3"}} - {{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}} - </h4> - <div class="list"> - {{range .Activity.UnresolvedIssues}} - <p class="desc"> - <span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span> - #{{.Index}} - {{if .IsPull}} - <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> - {{else}} - <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> - {{end}} - {{TimeSinceUnix .UpdatedUnix ctx.Locale}} - </p> - {{end}} - </div> - {{end}} </div> </div> {{template "base/footer" .}} + diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 31cd5b23f6..30d1a3d78d 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -2,20 +2,20 @@ {{$revsFileLink := URLJoin .RepoLink "src" .BranchNameSubURL "/.git-blame-ignore-revs"}} {{if .UsesIgnoreRevs}} <div class="ui info message"> - <p>{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.blame.ignore_revs" $revsFileLink (print $revsFileLink "?bypass-blame-ignore=true")}}</p> </div> {{else}} <div class="ui error message"> - <p>{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.blame.ignore_revs.failed" $revsFileLink}}</p> </div> {{end}} {{end}} <div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content"> - <h4 class="file-header ui top attached header gt-df gt-ac gt-sb gt-fw"> - <div class="file-header-left gt-df gt-ac gt-py-3 gt-pr-4"> + <h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap"> + <div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4"> {{template "repo/file_info" .}} </div> - <div class="file-header-right file-actions gt-df gt-ac gt-fw"> + <div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap"> <div class="ui buttons"> <a class="ui tiny button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a> {{if not .IsViewCommit}} @@ -24,12 +24,15 @@ <a class="ui tiny button" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.normal_view"}}</a> <a class="ui tiny button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_history"}}</a> <button class="ui tiny button unescape-button">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> - <button class="ui tiny button escape-button gt-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> + <button class="ui tiny button escape-button tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> </div> </div> </h4> <div class="ui attached table unstackable segment"> <div class="file-view code-view unicode-escaped"> + {{if .IsFileTooLarge}} + {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} + {{else}} <table> <tbody> {{range $row := .BlameRows}} @@ -41,11 +44,11 @@ {{$row.Avatar}} </div> <div class="blame-message"> - <a href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}"> + <a class="suppressed tw-text-text" href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}"> {{$row.CommitMessage}} </a> </div> - <div class="blame-time"> + <div class="blame-time not-mobile"> {{$row.CommitSince}} </div> </div> @@ -53,7 +56,7 @@ </td> <td class="lines-blame-btn"> {{if $row.PreviousSha}} - <a href="{{$row.PreviousShaURL}}" data-tooltip-content='{{ctx.Locale.Tr "repo.blame_prior"}}'> + <a role="button" class="muted" href="{{$row.PreviousShaURL}}" data-tooltip-content='{{ctx.Locale.Tr "repo.blame_prior"}}'> {{svg "octicon-versions"}} </a> {{end}} @@ -69,12 +72,19 @@ </td> {{end}} <td rel="L{{$row.RowNumber}}" class="lines-code blame-code chroma"> - <code class="code-inner gt-pl-3">{{$row.Code}}</code> + <code class="code-inner tw-pl-2">{{$row.Code}}</code> </td> </tr> {{end}} </tbody> </table> + {{end}}{{/* end if .IsFileTooLarge */}} + <div class="code-line-menu tippy-target"> + {{if $.Permission.CanRead $.UnitTypeIssues}} + <a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a> + {{end}} + <a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a> + </div> </div> </div> </div> diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 8ae7301c4a..77cccd65b7 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -22,14 +22,14 @@ <div class="flex-text-block"> {{if .DefaultBranchBranch.IsProtected}}{{svg "octicon-shield-lock"}}{{end}} <a class="gt-ellipsis" href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a> - <button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> + <button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}} </div> - <p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> + <p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> </td> <td class="right aligned middle aligned overflow-visible"> {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}} - <button class="btn interact-bg show-create-branch-modal gt-p-3" + <button class="btn interact-bg show-create-branch-modal tw-p-2" data-modal="#create-branch-modal" data-branch-from="{{$.DefaultBranchBranch.DBBranch.Name}}" data-branch-from-urlcomponent="{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}" @@ -39,10 +39,10 @@ </button> {{end}} {{if .EnableFeed}} - <a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-rss"}}</a> + <a role="button" class="btn interact-bg tw-p-2" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-rss"}}</a> {{end}} {{if not $.DisableDownloadSourceArchives}} - <div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" ($.DefaultBranchBranch.DBBranch.Name)}}"> + <div class="ui dropdown btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" ($.DefaultBranchBranch.DBBranch.Name)}}"> {{svg "octicon-download"}} <div class="menu"> <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a> @@ -51,7 +51,7 @@ </div> {{end}} {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted) (not $.IsMirror)}} - <button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal" + <button class="btn interact-bg tw-p-2 show-modal show-rename-branch-modal" data-is-default-branch="true" data-modal="#rename-branch-modal" data-old-branch-name="{{$.DefaultBranchBranch.DBBranch.Name}}" @@ -67,140 +67,136 @@ </div> {{end}} - {{if .Branches}} - <h4 class="ui top attached header gt-df gt-ac gt-sb"> - <div class="gt-df gt-ac"> - {{ctx.Locale.Tr "repo.branches"}} - </div> - <div class="gt-whitespace-nowrap"> - <form class="ignore-dirty" method="get"> - <div class="ui tiny search input"> - <input name="q" placeholder="{{ctx.Locale.Tr "repo.branch.search"}}" value="{{.Keyword}}" autofocus> - </div> - <button class="ui primary tiny button gt-mr-0" data-tooltip-content={{ctx.Locale.Tr "repo.commits.search.tooltip"}}>{{ctx.Locale.Tr "repo.commits.find"}}</button> - </form> - </div> - </h4> - - <div class="ui attached table segment"> - <table class="ui very basic striped fixed table single line"> - <tbody> - {{range .Branches}} - <tr> - <td class="eight wide"> - {{if .DBBranch.IsDeleted}} - <div class="flex-text-block"> - <a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a> - <button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> - </div> - <p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix ctx.Locale}}</p> - {{else}} - <div class="flex-text-block"> - {{if .IsProtected}}{{svg "octicon-shield-lock"}}{{end}} - <a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a> - <button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> - {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} - </div> - <p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p> - {{end}} - </td> - <td class="two wide ui"> - {{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}} - <div class="commit-divergence"> - <div class="bar-group"> - <div class="count count-behind">{{.CommitsBehind}}</div> - {{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}} - <div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> - </div> - <div class="bar-group"> - <div class="count count-ahead">{{.CommitsAhead}}</div> - <div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> - </div> - </div> - {{end}} - </td> - <td class="two wide right aligned"> - {{if not .LatestPullRequest}} - {{if .IsIncluded}} - <span class="ui orange large label" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.included_desc"}}"> - {{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}} - </span> - {{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} - <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}"> - <button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button> - </a> - {{end}} - {{else if and .LatestPullRequest.HasMerged .MergeMovedOn}} - {{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} - <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}"> - <button id="new-pull-request" class="ui compact basic button gt-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button> - </a> - {{end}} - {{else}} - <a href="{{.LatestPullRequest.Issue.Link}}" class="gt-vm ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a> - {{if .LatestPullRequest.HasMerged}} - <a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a> - {{else if .LatestPullRequest.Issue.IsClosed}} - <a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a> - {{else}} - <a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a> - {{end}} - {{end}} - </td> - <td class="three wide right aligned overflow-visible"> - {{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}} - <button class="btn interact-bg gt-p-3 show-modal show-create-branch-modal" - data-branch-from="{{.DBBranch.Name}}" - data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}" - data-tooltip-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}" - data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}" - > - {{svg "octicon-git-branch"}} - </button> - {{end}} - {{if $.EnableFeed}} - <a role="button" class="btn interact-bg gt-p-3" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a> - {{end}} - {{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}} - <div class="ui dropdown btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}"> - {{svg "octicon-download"}} - <div class="menu"> - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a> - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a> - </div> - </div> - {{end}} - {{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}} - <button class="btn interact-bg gt-p-3 show-modal show-rename-branch-modal" - data-is-default-branch="false" - data-old-branch-name="{{.DBBranch.Name}}" - data-modal="#rename-branch-modal" - data-tooltip-content="{{ctx.Locale.Tr "repo.branch.rename" (.DBBranch.Name)}}" - > - {{svg "octicon-pencil"}} - </button> - {{end}} - {{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}} - {{if .DBBranch.IsDeleted}} - <button class="btn interact-bg gt-p-3 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}"> - <span class="text blue"> - {{svg "octicon-reply"}} - </span> - </button> - {{else}} - <button class="btn interact-bg gt-p-3 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}"> - {{svg "octicon-trash"}} - </button> - {{end}} - {{end}} - </td> - </tr> - {{end}} - </tbody> - </table> + <h4 class="ui top attached header tw-flex tw-items-center tw-justify-between"> + <div class="tw-flex tw-items-center"> + {{ctx.Locale.Tr "repo.branches"}} </div> - {{template "base/paginate" .}} - {{end}} + </h4> + + <div class="ui attached segment"> + <form class="ignore-dirty" method="get"> + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.branch_kind")}} + </form> + </div> + + <div class="ui attached table segment"> + <table class="ui very basic striped fixed table single line"> + <tbody> + {{range .Branches}} + <tr> + <td class="eight wide"> + {{if .DBBranch.IsDeleted}} + <div class="flex-text-block"> + <a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a> + <button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> + </div> + <p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{TimeSinceUnix .DBBranch.DeletedUnix ctx.Locale}}</p> + {{else}} + <div class="flex-text-block"> + {{if .IsProtected}}{{svg "octicon-shield-lock"}}{{end}} + <a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a> + <button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button> + {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} + </div> + <p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p> + {{end}} + </td> + <td class="two wide ui"> + {{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}} + <div class="commit-divergence"> + <div class="bar-group"> + <div class="count count-behind">{{.CommitsBehind}}</div> + {{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}} + <div class="bar bar-behind" style="width: {{Eval 100 "*" .CommitsBehind "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> + </div> + <div class="bar-group"> + <div class="count count-ahead">{{.CommitsAhead}}</div> + <div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> + </div> + </div> + {{end}} + </td> + <td class="two wide right aligned"> + {{if not .LatestPullRequest}} + {{if .IsIncluded}} + <span class="ui orange large label" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.included_desc"}}"> + {{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.branch.included"}} + </span> + {{else if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} + <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}"> + <button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button> + </a> + {{end}} + {{else if and .LatestPullRequest.HasMerged .MergeMovedOn}} + {{if and (not .DBBranch.IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} + <a href="{{$.RepoLink}}/compare/{{PathEscapeSegments $.DefaultBranchBranch.DBBranch.Name}}...{{if ne $.Repository.Owner.Name $.Owner.Name}}{{PathEscape $.Owner.Name}}:{{end}}{{PathEscapeSegments .DBBranch.Name}}"> + <button id="new-pull-request" class="ui compact basic button tw-mr-0">{{if $.CanPull}}{{ctx.Locale.Tr "repo.pulls.compare_changes"}}{{else}}{{ctx.Locale.Tr "action.compare_branch"}}{{end}}</button> + </a> + {{end}} + {{else}} + <a href="{{.LatestPullRequest.Issue.Link}}" class="tw-align-middle ref-issue">{{if not .LatestPullRequest.IsSameRepo}}{{.LatestPullRequest.BaseRepo.FullName}}{{end}}#{{.LatestPullRequest.Issue.Index}}</a> + {{if .LatestPullRequest.HasMerged}} + <a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a> + {{else if .LatestPullRequest.Issue.IsClosed}} + <a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a> + {{else}} + <a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a> + {{end}} + {{end}} + </td> + <td class="three wide right aligned overflow-visible"> + {{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}} + <button class="btn interact-bg tw-p-2 show-modal show-create-branch-modal" + data-branch-from="{{.DBBranch.Name}}" + data-branch-from-urlcomponent="{{PathEscapeSegments .DBBranch.Name}}" + data-tooltip-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" .DBBranch.Name}}" + data-modal="#create-branch-modal" data-name="{{.DBBranch.Name}}" + > + {{svg "octicon-git-branch"}} + </button> + {{end}} + {{if $.EnableFeed}} + <a role="button" class="btn interact-bg tw-p-2" href="{{$.FeedURL}}/rss/branch/{{PathEscapeSegments .DBBranch.Name}}">{{svg "octicon-rss"}}</a> + {{end}} + {{if and (not .DBBranch.IsDeleted) (not $.DisableDownloadSourceArchives)}} + <div class="ui dropdown btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.download" (.DBBranch.Name)}}"> + {{svg "octicon-download"}} + <div class="menu"> + <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a> + <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments .DBBranch.Name}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a> + </div> + </div> + {{end}} + {{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted) (not $.IsMirror)}} + <button class="btn interact-bg tw-p-2 show-modal show-rename-branch-modal" + data-is-default-branch="false" + data-old-branch-name="{{.DBBranch.Name}}" + data-modal="#rename-branch-modal" + data-tooltip-content="{{ctx.Locale.Tr "repo.branch.rename" (.DBBranch.Name)}}" + > + {{svg "octicon-pencil"}} + </button> + {{end}} + {{if and $.IsWriter (not $.IsMirror) (not $.Repository.IsArchived) (not .IsProtected)}} + {{if .DBBranch.IsDeleted}} + <button class="btn interact-bg tw-p-2 link-action restore-branch-button" data-url="{{$.Link}}/restore?branch_id={{.DBBranch.ID}}&name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.restore" (.DBBranch.Name)}}"> + <span class="text blue"> + {{svg "octicon-reply"}} + </span> + </button> + {{else}} + <button class="btn interact-bg tw-p-2 delete-button delete-branch-button" data-url="{{$.Link}}/delete?name={{.DBBranch.Name}}&page={{$.Page.Paginater.Current}}" data-tooltip-content="{{ctx.Locale.Tr "repo.branch.delete" (.DBBranch.Name)}}" data-name="{{.DBBranch.Name}}"> + {{svg "octicon-trash"}} + </button> + {{end}} + {{end}} + </td> + </tr> + {{end}} + </tbody> + </table> + </div> + {{template "base/paginate" .}} </div> </div> @@ -210,7 +206,7 @@ {{ctx.Locale.Tr "repo.branch.delete_html"}} <span class="name"></span> </div> <div class="content"> - <p>{{ctx.Locale.Tr "repo.branch.delete_desc" | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index bee5363296..6c2e08a985 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -1,7 +1,7 @@ {{/* Attributes: * root * ContainerClasses -* (TODO: search "branch_dropdown" in the template direcotry) +* (TODO: search "branch_dropdown" in the template directory) */}} {{$defaultSelectedRefName := $.root.BranchName}} {{if and .root.IsViewTag (not .noTag)}} @@ -56,7 +56,7 @@ 'repoLink': {{.root.RepoLink}}, 'treePath': {{.root.TreePath}}, 'branchNameSubURL': {{.root.BranchNameSubURL}}, - 'noResults': {{ctx.Locale.Tr "repo.pulls.no_results"}}, + 'noResults': {{ctx.Locale.Tr "no_results_found"}}, }; {{if .release}} data.release = { @@ -70,8 +70,8 @@ <div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"> {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} <div class="ui dropdown custom"> - <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0"> - <span class="text gt-df gt-ac gt-mr-2"> + <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0"> + <span class="text tw-flex tw-items-center tw-mr-1"> {{if .release}} {{ctx.Locale.Tr "repo.release.compare"}} {{else}} @@ -80,7 +80,7 @@ {{else}} {{svg "octicon-git-branch"}} {{end}} - <strong ref="dropdownRefName" class="gt-ml-3">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong> + <strong ref="dropdownRefName" class="tw-ml-2">{{if and .root.IsViewTag (not .noTag)}}{{.root.TagName}}{{else if .root.IsViewBranch}}{{.root.BranchName}}{{else}}{{ShortSha .root.CommitID}}{{end}}</strong> {{end}} </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} diff --git a/templates/repo/cite/cite_buttons.tmpl b/templates/repo/cite/cite_buttons.tmpl index 9953c92c8a..426ca3858e 100644 --- a/templates/repo/cite/cite_buttons.tmpl +++ b/templates/repo/cite/cite_buttons.tmpl @@ -6,6 +6,6 @@ BibTeX </button> <!-- the value will be updated by initCitationFileCopyContent, the code below is used to avoid UI flicking --> <input id="citation-copy-content" value="" size="1" readonly> -<button class="ui icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="" data-clipboard-target="#citation-copy-content"> +<button class="ui icon button" id="citation-clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy"}}" data-clipboard-target="#citation-copy-content"> {{svg "octicon-copy"}} </button> diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl index c34c77e0c4..fb251442ca 100644 --- a/templates/repo/cite/cite_modal.tmpl +++ b/templates/repo/cite/cite_modal.tmpl @@ -1,16 +1,14 @@ -<div class="ui tiny modal" id="cite-repo-modal"> +<div class="ui small modal" id="cite-repo-modal"> <div class="header"> {{ctx.Locale.Tr "repo.cite_this_repo"}} </div> <div class="content"> <div class="ui stackable secondary menu"> - <div class="fitted item"> - <div class="ui action input" id="citation-panel"> - {{template "repo/cite/cite_buttons" .}} - <a id="goto-citation-btn" class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}"> - {{svg "octicon-file-moved"}} - </a> - </div> + <div class="ui action input" id="citation-panel"> + {{template "repo/cite/cite_buttons" .}} + <a id="goto-citation-btn" class="ui basic jump icon button" href="{{$.RepoLink}}/src/{{$.BranchName}}/CITATION.cff" data-tooltip-content="{{ctx.Locale.Tr "repo.find_file.go_to_file"}}"> + {{svg "octicon-file-moved"}} + </a> </div> </div> </div> diff --git a/templates/repo/clone_buttons.tmpl b/templates/repo/clone_buttons.tmpl index a664c4bda8..89daba9dc9 100644 --- a/templates/repo/clone_buttons.tmpl +++ b/templates/repo/clone_buttons.tmpl @@ -1,15 +1,15 @@ <!-- there is always at least one button (by context/repo.go) --> {{if $.CloneButtonShowHTTPS}} - <button class="ui small compact clone button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}"> + <button class="ui small button" id="repo-clone-https" data-link="{{$.CloneButtonOriginLink.HTTPS}}"> HTTPS </button> {{end}} {{if $.CloneButtonShowSSH}} - <button class="ui small compact clone button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}"> + <button class="ui small button" id="repo-clone-ssh" data-link="{{$.CloneButtonOriginLink.SSH}}"> SSH </button> {{end}} <input id="repo-clone-url" size="20" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly> -<button class="ui basic small compact icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}"> +<button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}"> {{svg "octicon-copy" 14}} </button> diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl index 0797b400d8..40dae76dc7 100644 --- a/templates/repo/clone_script.tmpl +++ b/templates/repo/clone_script.tmpl @@ -24,19 +24,27 @@ const btn = isSSH ? sshBtn : httpsBtn; if (!btn) return; - let link = btn.getAttribute('data-link'); - if (link.startsWith('http://') || link.startsWith('https://')) { - // use current protocol/host as the clone link - const url = new URL(link); - url.protocol = window.location.protocol; - url.host = window.location.host; - link = url.toString(); + // NOTE: Keep this function in sync with the one in the js folder + function toOriginUrl(urlStr) { + try { + if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) { + const {origin, protocol, hostname, port} = window.location; + const url = new URL(urlStr, origin); + url.protocol = protocol; + url.hostname = hostname; + url.port = port || (protocol === 'https:' ? '443' : '80'); + return url.toString(); + } + } catch {} + return urlStr; } + const link = toOriginUrl(btn.getAttribute('data-link')); + for (const el of document.getElementsByClassName('js-clone-url')) { el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link; } - for (const el of document.getElementsByClassName('js-clone-url-vsc')) { - el['href'] = 'vscode://vscode.git/clone?url=' + encodeURIComponent(link); + for (const el of document.getElementsByClassName('js-clone-url-editor')) { + el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link)); } })(); </script> diff --git a/templates/repo/code/recently_pushed_new_branches.tmpl b/templates/repo/code/recently_pushed_new_branches.tmpl index 8910a9e5b6..17ae7d119d 100644 --- a/templates/repo/code/recently_pushed_new_branches.tmpl +++ b/templates/repo/code/recently_pushed_new_branches.tmpl @@ -1,10 +1,11 @@ {{range .RecentlyPushedNewBranches}} - <div class="ui positive message gt-df gt-ac"> - <div class="gt-f1"> + <div class="ui positive message tw-flex tw-items-center"> + <div class="tw-flex-1"> {{$timeSince := TimeSince .CommitTime.AsTime ctx.Locale}} - {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" (Escape .Name) $timeSince | Safe}} + {{$branchLink := HTMLFormat `<a href="%s/src/branch/%s">%s</a>` $.RepoLink (PathEscapeSegments .Name) .Name}} + {{ctx.Locale.Tr "repo.pulls.recently_pushed_new_branches" $branchLink $timeSince}} </div> - <a role="button" class="ui compact positive button gt-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}"> + <a role="button" class="ui compact positive button tw-m-0" href="{{$.Repository.ComposeBranchCompareURL $.Repository.BaseRepo .Name}}"> {{ctx.Locale.Tr "repo.pulls.compare_changes"}} </a> </div> diff --git a/templates/repo/code_frequency.tmpl b/templates/repo/code_frequency.tmpl new file mode 100644 index 0000000000..50ec1beb6b --- /dev/null +++ b/templates/repo/code_frequency.tmpl @@ -0,0 +1,9 @@ +{{if .Permission.CanRead $.UnitTypeCode}} + <div id="repo-code-frequency-chart" + data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.code_frequency.what")}}" + data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.code_frequency.what")}}" + data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}" + data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}" + > + </div> +{{end}} diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl index 883230ac29..ffa0e530e8 100644 --- a/templates/repo/commit_load_branches_and_tags.tmpl +++ b/templates/repo/commit_load_branches_and_tags.tmpl @@ -1,19 +1,19 @@ {{if not .PageIsWiki}} <div class="branch-and-tag-area" data-text-default-branch-tooltip="{{ctx.Locale.Tr "repo.commit.contained_in_default_branch"}}"> - <button class="ui button ellipsis-button load-branches-and-tags gt-mt-3" aria-expanded="false" + <button class="ui button ellipsis-button load-branches-and-tags tw-mt-2" aria-expanded="false" data-fetch-url="{{.RepoLink}}/commit/{{.CommitID}}/load-branches-and-tags" data-tooltip-content="{{ctx.Locale.Tr "repo.commit.load_referencing_branches_and_tags"}}" >...</button> - <div class="branch-and-tag-detail gt-hidden"> + <div class="branch-and-tag-detail tw-hidden"> <div class="divider"></div> <div>{{ctx.Locale.Tr "repo.commit.contained_in"}}</div> - <div class="gt-df gt-mt-3"> - <div class="gt-p-2">{{svg "octicon-git-branch"}}</div> - <div class="branch-area flex-text-block gt-fw gt-f1"></div> + <div class="tw-flex tw-mt-2"> + <div class="tw-p-1">{{svg "octicon-git-branch"}}</div> + <div class="branch-area flex-text-block tw-flex-wrap tw-flex-1"></div> </div> - <div class="gt-df gt-mt-3"> - <div class="gt-p-2">{{svg "octicon-tag"}}</div> - <div class="tag-area flex-text-block gt-fw gt-f1"></div> + <div class="tw-flex tw-mt-2"> + <div class="tw-p-1">{{svg "octicon-tag"}}</div> + <div class="tag-area flex-text-block tw-flex-wrap tw-flex-1"></div> </div> </div> </div> diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 01fa45babe..7fec88cb79 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -17,11 +17,11 @@ {{$class = (print $class " isWarning")}} {{end}} {{end}} - <div class="ui top attached header clearing segment gt-relative commit-header {{$class}}"> - <div class="gt-df gt-mb-4 gt-fw"> - <h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> + <div class="ui top attached header clearing segment tw-relative commit-header {{$class}}"> + <div class="tw-flex tw-mb-4 tw-gap-1"> + <h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> {{if not $.PageIsWiki}} - <div> + <div class="commit-header-buttons"> <a class="ui primary tiny button" href="{{.SourcePath}}"> {{ctx.Locale.Tr "repo.diff.browse_source"}} </a> @@ -88,7 +88,7 @@ {{.CsrfTokenHtml}} <div class="field"> <label> - {{ctx.Locale.Tr "repo.branch.new_branch_from" `<span class="text" id="modal-create-branch-from-span"></span>` | Safe}} + {{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|SafeHTML)}} </label> </div> <div class="required field"> @@ -113,7 +113,7 @@ <input type="hidden" name="create_tag" value="true"> <div class="field"> <label> - {{ctx.Locale.Tr "repo.tag.create_tag_from" `<span class="text" id="modal-create-tag-from-span"></span>` | Safe}} + {{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|SafeHTML)}} </label> </div> <div class="required field"> @@ -139,34 +139,34 @@ {{end}} {{template "repo/commit_load_branches_and_tags" .}} </div> - <div class="ui attached segment gt-df gt-ac gt-sb gt-py-2 commit-header-row gt-fw {{$class}}"> - <div class="gt-df gt-ac author"> + <div class="ui attached segment tw-flex tw-items-center tw-justify-between tw-py-1 commit-header-row tw-flex-wrap {{$class}}"> + <div class="tw-flex tw-items-center author"> {{if .Author}} - {{ctx.AvatarUtils.Avatar .Author 28 "gt-mr-3"}} + {{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}} {{if .Author.FullName}} <a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a> {{else}} <a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a> {{end}} {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "gt-mr-3"}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}} <strong>{{.Commit.Author.Name}}</strong> {{end}} - <span class="text grey gt-ml-3" id="authored-time">{{TimeSince .Commit.Author.When ctx.Locale}}</span> + <span class="text grey tw-ml-2" id="authored-time">{{TimeSince .Commit.Author.When ctx.Locale}}</span> {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} - <span class="text grey gt-mx-3">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span> + <span class="text grey tw-mx-2">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span> {{if ne .Verification.CommittingUser.ID 0}} - {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "gt-mx-3"}} + {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}} <a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "gt-mr-3"}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}} <strong>{{.Commit.Committer.Name}}</strong> {{end}} {{end}} </div> - <div class="ui horizontal list gt-df gt-ac"> + <div class="tw-flex tw-items-center"> {{if .Parents}} - <div class="item"> + <div> <span>{{ctx.Locale.Tr "repo.diff.parent"}}</span> {{range .Parents}} {{if $.PageIsWiki}} @@ -184,73 +184,73 @@ </div> </div> {{if .Commit.Signature}} - <div class="ui bottom attached message gt-text-left gt-df gt-ac gt-sb commit-header-row gt-fw {{$class}}"> - <div class="gt-df gt-ac"> + <div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap tw-mb-0 {{$class}}"> + <div class="tw-flex tw-items-center"> {{if .Verification.Verified}} {{if ne .Verification.SigningUser.ID 0}} - {{svg "gitea-lock" 16 "gt-mr-3"}} + {{svg "gitea-lock" 16 "tw-mr-2"}} {{if eq .Verification.TrustStatus "trusted"}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span> {{else if eq .Verification.TrustStatus "untrusted"}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span> {{else}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span> {{end}} - {{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "gt-mr-3"}} + {{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}} <a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.GetDisplayName}}</strong></a> {{else}} - <span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "gt-mr-3"}}</span> - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span> - {{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "gt-mr-3"}} + <span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "tw-mr-2"}}</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span> + {{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}} <strong>{{.Verification.SigningUser.GetDisplayName}}</strong> {{end}} {{else}} - {{svg "gitea-unlock" 16 "gt-mr-3"}} + {{svg "gitea-unlock" 16 "tw-mr-2"}} <span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span> {{end}} </div> - <div class="gt-df gt-ac"> + <div class="tw-flex tw-items-center"> {{if .Verification.Verified}} {{if ne .Verification.SigningUser.ID 0}} - {{svg "octicon-verified" 16 "gt-mr-3"}} + {{svg "octicon-verified" 16 "tw-mr-2"}} {{if .Verification.SigningSSHKey}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> {{.Verification.SigningSSHKey.Fingerprint}} {{else}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.PaddedKeyID}} {{end}} {{else}} - {{svg "octicon-unverified" 16 "gt-mr-3"}} + {{svg "octicon-unverified" 16 "tw-mr-2"}} {{if .Verification.SigningSSHKey}} - <span class="ui text gt-mr-3" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> + <span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> {{.Verification.SigningSSHKey.Fingerprint}} {{else}} - <span class="ui text gt-mr-3" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> + <span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.PaddedKeyID}} {{end}} {{end}} {{else if .Verification.Warning}} - {{svg "octicon-unverified" 16 "gt-mr-3"}} + {{svg "octicon-unverified" 16 "tw-mr-2"}} {{if .Verification.SigningSSHKey}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> {{.Verification.SigningSSHKey.Fingerprint}} {{else}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.PaddedKeyID}} {{end}} {{else}} {{if .Verification.SigningKey}} {{if ne .Verification.SigningKey.KeyID ""}} - {{svg "octicon-verified" 16 "gt-mr-3"}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> + {{svg "octicon-verified" 16 "tw-mr-2"}} + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.PaddedKeyID}} {{end}} {{end}} {{if .Verification.SigningSSHKey}} {{if ne .Verification.SigningSSHKey.Fingerprint ""}} - {{svg "octicon-verified" 16 "gt-mr-3"}} - <span class="ui text gt-mr-3">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> + {{svg "octicon-verified" 16 "tw-mr-2"}} + <span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span> {{.Verification.SigningSSHKey.Fingerprint}} {{end}} {{end}} @@ -260,7 +260,7 @@ {{end}} {{if .NoteRendered}} <div class="ui top attached header segment git-notes"> - {{svg "octicon-note" 16 "gt-mr-3"}} + {{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.diff.git-notes"}}: {{if .NoteAuthor}} <a href="{{.NoteAuthor.HomeLink}}"> @@ -276,7 +276,7 @@ <span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span> </div> <div class="ui bottom attached info segment git-notes"> - <pre class="commit-body">{{.NoteRendered | Str2html}}</pre> + <pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre> </div> {{end}} {{template "repo/diff/box" .}} diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl index ec2be6c38d..f451ac06a1 100644 --- a/templates/repo/commit_statuses.tmpl +++ b/templates/repo/commit_statuses.tmpl @@ -1,10 +1,10 @@ {{if .Statuses}} {{if and (eq (len .Statuses) 1) .Status.TargetURL}} - <a class="gt-vm gt-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}"> + <a class="tw-align-middle {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}"> {{template "repo/commit_status" .Status}} </a> {{else}} - <span class="gt-vm" data-tippy="commit-statuses" tabindex="0"> + <span class="tw-align-middle {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0"> {{template "repo/commit_status" .Status}} </span> {{end}} diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl index 42004c2610..e6efe1ff54 100644 --- a/templates/repo/commits.tmpl +++ b/templates/repo/commits.tmpl @@ -4,8 +4,8 @@ <div class="ui container"> {{template "repo/sub_menu" .}} <div class="repo-button-row"> - <div class="gt-df gt-ac"> - {{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}} + <div class="tw-flex tw-items-center"> + {{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}} <a href="{{.RepoLink}}/graph" class="ui basic small compact button"> {{svg "octicon-git-branch"}} {{ctx.Locale.Tr "repo.commit_graph"}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 7702770c40..bb5d2a0394 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -1,89 +1,93 @@ <div class="ui attached table segment commit-table"> - <table class="ui very basic striped table unstackable" id="commits-table"> - <thead> + <table class="ui very basic striped table unstackable" id="commits-table"> + <thead> + <tr> + <th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th> + <th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th> + <th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th> + <th class="two wide right aligned">{{ctx.Locale.Tr "repo.commits.date"}}</th> + <th class="one wide"></th> + </tr> + </thead> + <tbody class="commit-list"> + {{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}} + {{range .Commits}} <tr> - <th class="three wide">{{ctx.Locale.Tr "repo.commits.author"}}</th> - <th class="two wide sha">{{StringUtils.ToUpper $.Repository.ObjectFormatName}}</th> - <th class="eight wide message">{{ctx.Locale.Tr "repo.commits.message"}}</th> - <th class="two wide right aligned">{{ctx.Locale.Tr "repo.commits.date"}}</th> - <th class="one wide"></th> - </tr> - </thead> - <tbody class="commit-list"> - {{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}} - {{range .Commits}} - <tr> - <td class="author"> + <td class="author"> + <div class="tw-flex"> {{$userName := .Author.Name}} {{if .User}} - {{if .User.FullName}} + {{if and .User.FullName DefaultShowFullName}} {{$userName = .User.FullName}} {{end}} - {{ctx.AvatarUtils.Avatar .User 28 "gt-mr-2"}}<a href="{{.User.HomeLink}}">{{$userName}}</a> + {{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}<a class="muted author-wrapper" href="{{.User.HomeLink}}">{{$userName}}</a> {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "gt-mr-2"}} - {{$userName}} + {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 28 "tw-mr-2"}} + <span class="author-wrapper">{{$userName}}</span> {{end}} - </td> - <td class="sha"> - {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} + </div> + </td> + <td class="sha"> + {{$class := "ui sha label"}} + {{if .Signature}} + {{$class = (print $class " isSigned")}} + {{if .Verification.Verified}} + {{if eq .Verification.TrustStatus "trusted"}} + {{$class = (print $class " isVerified")}} + {{else if eq .Verification.TrustStatus "untrusted"}} + {{$class = (print $class " isVerifiedUntrusted")}} + {{else}} + {{$class = (print $class " isVerifiedUnmatched")}} {{end}} + {{else if .Verification.Warning}} + {{$class = (print $class " isWarning")}} {{end}} - {{$commitShaLink := ""}} - {{if $.PageIsWiki}} - {{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}} - {{else if $.PageIsPullCommits}} - {{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}} - {{else if $.Reponame}} - {{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}} - {{end}} - <a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}"> - <span class="shortsha">{{ShortSha .ID.String}}</span> - {{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}} - </a> - </td> - <td class="message"> - <span class="message-wrapper"> - {{if $.PageIsWiki}} - <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span> - {{else}} - {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} - <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span> - {{end}} - </span> - {{if IsMultilineCommitMessage .Message}} - <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> - {{end}} - {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{if IsMultilineCommitMessage .Message}} - <pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre> - {{end}} - </td> - {{if .Committer}} - <td class="text right aligned">{{TimeSince .Committer.When ctx.Locale}}</td> - {{else}} - <td class="text right aligned">{{TimeSince .Author.When ctx.Locale}}</td> {{end}} - <td class="text right aligned gt-py-0"> - <button class="btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button> - {{if $.FileName}} - <a class="btn interact-bg gt-p-3" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{printf "%s/src/commit/%s/%s" $commitRepoLink (PathEscape .ID.String) (PathEscapeSegments $.FileName)}}">{{svg "octicon-file-code"}}</a> - {{end}} - </td> - </tr> - {{end}} - </tbody> - </table> - </div> + {{$commitShaLink := ""}} + {{if $.PageIsWiki}} + {{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{else if $.PageIsPullCommits}} + {{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}} + {{else if $.Reponame}} + {{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{end}} + <a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}"> + <span class="shortsha">{{ShortSha .ID.String}}</span> + {{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}} + </a> + </td> + <td class="message"> + <span class="message-wrapper"> + {{if $.PageIsWiki}} + <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span> + {{else}} + {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} + <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span> + {{end}} + </span> + {{if IsMultilineCommitMessage .Message}} + <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> + {{end}} + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} + {{if IsMultilineCommitMessage .Message}} + <pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .Message ($.Repository.ComposeMetas ctx)}}</pre> + {{end}} + </td> + {{if .Committer}} + <td class="text right aligned">{{TimeSince .Committer.When ctx.Locale}}</td> + {{else}} + <td class="text right aligned">{{TimeSince .Author.When ctx.Locale}}</td> + {{end}} + <td class="text right aligned tw-py-0"> + <button class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "copy_hash"}}" data-clipboard-text="{{.ID}}">{{svg "octicon-copy"}}</button> + {{if not $.PageIsWiki}}{{/* at the moment, wiki doesn't support "view at history point*/}} + {{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}} + {{if $.FileName}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileName)}}{{end}} + <a class="btn interact-bg tw-p-2" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{$viewCommitLink}}">{{svg "octicon-file-code"}}</a> + {{end}} + </td> + </tr> + {{end}} + </tbody> + </table> +</div> diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 79e1bd6309..d96b314d01 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -13,7 +13,7 @@ {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} - <span class="shabox gt-df gt-ac gt-float-right"> + <span class="shabox tw-flex tw-items-center tw-float-right"> {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} {{$class := "ui sha label"}} {{if .Signature}} @@ -30,7 +30,7 @@ {{$class = (print $class " isWarning")}} {{end}} {{end}} - <a href="{{$commitLink}}" rel="nofollow" class="gt-ml-3 {{$class}}"> + <a href="{{$commitLink}}" rel="nofollow" class="tw-ml-2 {{$class}}"> <span class="shortsha">{{ShortSha .ID.String}}</span> {{if .Signature}} {{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}} @@ -38,12 +38,12 @@ </a> </span> - <span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span> + <span class="tw-font-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</span> {{if IsMultilineCommitMessage .Message}} <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> {{end}} {{if IsMultilineCommitMessage .Message}} - <pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre> + <pre class="commit-body tw-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx)}}</pre> {{end}} </div> {{end}} diff --git a/templates/repo/commits_search_dropdown.tmpl b/templates/repo/commits_search_dropdown.tmpl new file mode 100644 index 0000000000..5aa3f4f320 --- /dev/null +++ b/templates/repo/commits_search_dropdown.tmpl @@ -0,0 +1,8 @@ +<div class="ui small dropdown selection"> + <input name="all" type="hidden" value="{{.All}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="text">{{if .All}}{{ctx.Locale.Tr "repo.commits.search_all"}}{{else}}{{ctx.Locale.Tr "repo.commits.search_branch"}}{{end}}</div> + <div class="menu"> + <div class="item" data-value="false">{{ctx.Locale.Tr "repo.commits.search_branch"}}</div> + <div class="item" data-value="true">{{ctx.Locale.Tr "repo.commits.search_all"}}</div> + </div> +</div> diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 054a3f6bec..91fc1c2fae 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -1,5 +1,5 @@ -<h4 class="ui top attached header commits-table gt-df gt-ac gt-sb"> - <div class="commits-table-left gt-df gt-ac"> +<h4 class="ui top attached header commits-table tw-flex tw-items-center tw-justify-between"> + <div class="commits-table-left tw-flex tw-items-center"> {{if or .PageIsCommits (gt .CommitCount 0)}} {{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}} {{else if .IsNothingToCompare}} @@ -8,27 +8,27 @@ {{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}} {{end}} </div> - <div class="commits-table-right gt-whitespace-nowrap"> - {{if .PageIsCommits}} - <form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search"> - <div class="ui tiny search input"> - <input name="q" placeholder="{{ctx.Locale.Tr "repo.commits.search"}}" value="{{.Keyword}}" autofocus> - </div> - - <div class="ui tiny checkbox"> - <input type="checkbox" name="all" value="true" {{.All}}> - <label>{{ctx.Locale.Tr "repo.commits.search_all"}}</label> - </div> - <button class="ui primary tiny button gt-mr-0" data-panel="#add-deploy-key-panel" data-tooltip-content={{ctx.Locale.Tr "repo.commits.search.tooltip"}}>{{ctx.Locale.Tr "repo.commits.find"}}</button> - </form> - {{else if .IsDiffCompare}} - <a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a> + {{if .IsDiffCompare}} + <div class="commits-table-right tw-whitespace-nowrap"> + <a href="{{$.CommitRepoLink}}/commit/{{.BeforeCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .BaseIsCommit}}{{if .BaseIsBranch}}{{svg "octicon-git-branch"}}{{else if .BaseIsTag}}{{svg "octicon-tag"}}{{end}}{{.BaseBranch}}{{else}}{{ShortSha .BaseBranch}}{{end}}</a> ... - <a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label gt-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a> - {{end}} - </div> + <a href="{{$.CommitRepoLink}}/commit/{{.AfterCommitID | PathEscape}}" class="ui green sha label tw-mx-0">{{if not .HeadIsCommit}}{{if .HeadIsBranch}}{{svg "octicon-git-branch"}}{{else if .HeadIsTag}}{{svg "octicon-tag"}}{{end}}{{.HeadBranch}}{{else}}{{ShortSha .HeadBranch}}{{end}}</a> + </div> + {{end}} </h4> +{{if .PageIsCommits}} + <div class="ui attached segment"> + <form class="ignore-dirty" action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search"> + <div class="ui small fluid action input"> + {{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.commit_kind")}} + {{template "repo/commits_search_dropdown" .}} + {{template "shared/search/button" dict "Tooltip" (ctx.Locale.Tr "repo.commits.search.tooltip")}} + </div> + </form> + </div> +{{end}} + {{if and .Commits (gt .CommitCount 0)}} {{template "repo/commits_list" .}} {{end}} diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl new file mode 100644 index 0000000000..4a258e5b70 --- /dev/null +++ b/templates/repo/contributors.tmpl @@ -0,0 +1,13 @@ +{{if .Permission.CanRead $.UnitTypeCode}} + <div id="repo-contributors-chart" + data-locale-filter-label="{{ctx.Locale.Tr "repo.contributors.contribution_type.filter_label"}}" + data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}" + data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}" + data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}" + data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.contributors.what")}}" + data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.contributors.what")}}" + data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}" + data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}" + > + </div> +{{end}} diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 66f73fb398..bcd3c16b6a 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -51,10 +51,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> <span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span> @@ -73,7 +73,7 @@ </div> </div> - <div id="template_units" class="gt-hidden"> + <div id="template_units" class="tw-hidden"> <div class="inline field"> <label>{{ctx.Locale.Tr "repo.template.items"}}</label> <div class="ui checkbox"> @@ -158,7 +158,7 @@ {{end}} </div> </div> - <span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/" | Str2html}}</span> + <span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span> </div> <div class="inline field"> diff --git a/templates/repo/create_helper.tmpl b/templates/repo/create_helper.tmpl index 653955efc9..70c28b72e8 100644 --- a/templates/repo/create_helper.tmpl +++ b/templates/repo/create_helper.tmpl @@ -1,3 +1,3 @@ {{if not $.DisableMigrations}} - <p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" ((print AppSubUrl "/repo/migrate")|Escape) | Safe}}</p> + <p class="ui center">{{ctx.Locale.Tr "repo.new_repo_helper" (print AppSubUrl "/repo/migrate")}}</p> {{end}} diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl index 2dff28a965..8312b5d913 100644 --- a/templates/repo/diff/blob_excerpt.tmpl +++ b/templates/repo/diff/blob_excerpt.tmpl @@ -3,19 +3,19 @@ <tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}"> {{if eq .GetType 4}} <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"> - <div class="gt-df"> + <div class="tw-flex"> {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - <button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - <button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $line.GetExpandDirection 2}} - <button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> {{svg "octicon-fold"}} </button> {{end}} @@ -27,7 +27,7 @@ {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td> <td class="blob-excerpt lines-escape lines-escape-old">{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> - <td class="blob-excerpt lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="gt-mono" data-type-marker=""></span>{{end}}</td> + <td class="blob-excerpt lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td> <td class="blob-excerpt lines-code lines-code-old">{{/* */}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* */}}<code class="code-inner"></code>{{/* @@ -35,7 +35,7 @@ */}}</td> <td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td> <td class="blob-excerpt lines-escape lines-escape-new">{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> - <td class="blob-excerpt lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="gt-mono" data-type-marker=""></span>{{end}}</td> + <td class="blob-excerpt lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td> <td class="blob-excerpt lines-code lines-code-new">{{/* */}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* */}}<code class="code-inner"></code>{{/* @@ -49,19 +49,19 @@ <tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}"> {{if eq .GetType 4}} <td colspan="2" class="lines-num"> - <div class="gt-df"> + <div class="tw-flex"> {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - <button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - <button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $line.GetExpandDirection 2}} - <button class="code-expander-button" data-url="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.PageIsWiki}}" data-anchor="{{$.Anchor}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.RepoLink}}/blob_excerpt/{{PathEscape $.AfterCommitID}}?data-query={{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.PageIsWiki}}&anchor={{$.Anchor}}"> {{svg "octicon-fold"}} </button> {{end}} @@ -73,7 +73,7 @@ {{end}} {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} <td class="blob-excerpt lines-escape">{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> - <td class="blob-excerpt lines-type-marker"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> + <td class="blob-excerpt lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> <td class="blob-excerpt lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}"><code {{if $inlineDiff.EscapeStatus.Escaped}}class="code-inner has-escaped" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"{{else}}class="code-inner"{{end}}>{{$inlineDiff.Content}}</code></td> </tr> {{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index be7c7e80f2..92a3163642 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -1,31 +1,31 @@ {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}} <div> <div class="diff-detail-box diff-box"> - <div class="gt-df gt-ac gt-fw"> + <div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-ml-0.5"> {{if $showFileTree}} <button class="diff-toggle-file-tree-button not-mobile btn interact-fg" data-show-text="{{ctx.Locale.Tr "repo.diff.show_file_tree"}}" data-hide-text="{{ctx.Locale.Tr "repo.diff.hide_file_tree"}}"> {{/* the icon meaning is reversed here, "octicon-sidebar-collapse" means show the file tree */}} - {{svg "octicon-sidebar-collapse" 20 "icon gt-hidden"}} - {{svg "octicon-sidebar-expand" 20 "icon gt-hidden"}} + {{svg "octicon-sidebar-collapse" 20 "icon tw-hidden"}} + {{svg "octicon-sidebar-expand" 20 "icon tw-hidden"}} </button> <script> // Default to true if unset const diffTreeVisible = localStorage?.getItem('diff_file_tree_visible') !== 'false'; const diffTreeBtn = document.querySelector('.diff-toggle-file-tree-button'); const diffTreeIcon = `.octicon-sidebar-${diffTreeVisible ? 'expand' : 'collapse'}`; - diffTreeBtn.querySelector(diffTreeIcon).classList.remove('gt-hidden'); + diffTreeBtn.querySelector(diffTreeIcon).classList.remove('tw-hidden'); diffTreeBtn.setAttribute('data-tooltip-content', diffTreeBtn.getAttribute(diffTreeVisible ? 'data-hide-text' : 'data-show-text')); </script> {{end}} {{if not .DiffNotAvailable}} - <div class="diff-detail-stats gt-df gt-ac gt-fw"> - {{svg "octicon-diff" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2html}} + <div class="diff-detail-stats tw-flex tw-items-center tw-flex-wrap"> + {{svg "octicon-diff" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion}} </div> {{end}} </div> <div class="diff-detail-actions"> {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}} - <div class="not-mobile gt-df gt-ac gt-fc gt-whitespace-nowrap gt-mr-2"> + <div class="not-mobile tw-flex tw-items-center tw-flex-col tw-whitespace-nowrap tw-mr-1"> <label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{ctx.Locale.Tr "repo.pulls.viewed_files_label"}}"> {{ctx.Locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}} </label> @@ -68,7 +68,7 @@ binaryFileMessage: "{{ctx.Locale.Tr "repo.diff.bin"}}", showMoreMessage: "{{ctx.Locale.Tr "repo.diff.show_more"}}", statisticsMessage: "{{ctx.Locale.Tr "repo.diff.stats_desc_file"}}", - linkLoadMore: "{{$.Link}}?skip-to={{.Diff.End}}&file-only=true", + linkLoadMore: "?skip-to={{.Diff.End}}&file-only=true", }; // for first time loading, the diffFileInfo is a plain object @@ -89,9 +89,9 @@ {{end}} <div id="diff-container"> {{if $showFileTree}} - <div id="diff-file-tree" class="gt-hidden not-mobile"></div> + <div id="diff-file-tree" class="tw-hidden not-mobile"></div> <script> - if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('gt-hidden'); + if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden'); </script> {{end}} {{if .DiffNotAvailable}} @@ -109,42 +109,44 @@ {{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}} {{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}} {{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}} - <div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} gt-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}> - <h4 class="diff-file-header sticky-2nd-row ui top attached normal header gt-df gt-ac gt-sb gt-fw"> - <div class="diff-file-name gt-df gt-ac gt-gap-2 gt-fw"> - <button class="fold-file btn interact-bg gt-p-2{{if not $isExpandable}} gt-invisible{{end}}"> + <div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}> + <h4 class="diff-file-header sticky-2nd-row ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between tw-flex-wrap"> + <div class="diff-file-name tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-flex-wrap"> + <button class="fold-file btn interact-bg tw-p-1{{if not $isExpandable}} tw-invisible{{end}}"> {{if $file.ShouldBeHidden}} {{svg "octicon-chevron-right" 18}} {{else}} {{svg "octicon-chevron-down" 18}} {{end}} </button> - <div class="gt-font-semibold gt-df gt-ac gt-mono"> + <div class="tw-font-semibold tw-flex tw-items-center tw-font-mono"> {{if $file.IsBin}} - <span class="gt-ml-1 gt-mr-3"> + <span class="tw-ml-0.5 tw-mr-2"> {{ctx.Locale.Tr "repo.diff.bin"}} </span> {{else}} {{template "repo/diff/stats" dict "file" . "root" $}} {{end}} </div> - <span class="file gt-mono"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a>{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}</span> - <button class="btn interact-fg gt-p-3" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button> - {{if $file.IsGenerated}} - <span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span> - {{end}} - {{if $file.IsVendored}} - <span class="ui label">{{ctx.Locale.Tr "repo.diff.vendored"}}</span> - {{end}} - {{if and $file.Mode $file.OldMode}} - {{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}} - {{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}} - <span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span> - {{else if $file.Mode}} - <span class="gt-ml-4 gt-mono">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span> - {{end}} + <span class="file tw-flex tw-items-center tw-font-mono tw-flex-1"><a class="muted file-link" title="{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}" href="#diff-{{$file.NameHash}}">{{if $file.IsRenamed}}{{$file.OldName}} → {{end}}{{$file.Name}}</a> + {{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}} + <button class="btn interact-fg tw-p-2" data-clipboard-text="{{$file.Name}}">{{svg "octicon-copy" 14}}</button> + {{if $file.IsGenerated}} + <span class="ui label">{{ctx.Locale.Tr "repo.diff.generated"}}</span> + {{end}} + {{if $file.IsVendored}} + <span class="ui label">{{ctx.Locale.Tr "repo.diff.vendored"}}</span> + {{end}} + {{if and $file.Mode $file.OldMode}} + {{$old := ctx.Locale.Tr ($file.ModeTranslationKey $file.OldMode)}} + {{$new := ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}} + <span class="tw-mx-2 tw-font-mono tw-whitespace-nowrap">{{ctx.Locale.Tr "git.filemode.changed_filemode" $old $new}}</span> + {{else if $file.Mode}} + <span class="tw-mx-2 tw-font-mono tw-whitespace-nowrap">{{ctx.Locale.Tr ($file.ModeTranslationKey $file.Mode)}}</span> + {{end}} + </span> </div> - <div class="diff-file-header-actions gt-df gt-ac gt-gap-2 gt-fw"> + <div class="diff-file-header-actions tw-flex tw-items-center tw-gap-1 tw-flex-wrap"> {{if $showFileViewToggle}} <div class="ui compact icon buttons"> <button class="ui tiny basic button file-view-toggle" data-toggle-selector="#diff-source-{{$file.NameHash}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code"}}</button> @@ -159,13 +161,16 @@ {{end}} {{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}} <button class="ui basic tiny button unescape-button not-mobile">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> - <button class="ui basic tiny button escape-button gt-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> + <button class="ui basic tiny button escape-button tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> {{end}} {{if and (not $file.IsSubmodule) (not $.PageIsWiki)}} {{if $file.IsDeleted}} <a class="ui basic tiny button" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> {{else}} <a class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> + {{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}} + <a class="ui basic tiny button" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a> + {{end}} {{end}} {{end}} {{if $isReviewFile}} @@ -176,15 +181,15 @@ </div> </h4> <div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}> - <div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} gt-hidden{{end}}"> + <div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} tw-hidden{{end}}"> {{if or $file.IsIncomplete $file.IsBin}} - <div class="diff-file-body binary" style="padding: 5px 10px;"> + <div class="diff-file-body binary"> {{if $file.IsIncomplete}} {{if $file.IsIncompleteLineTooLong}} {{ctx.Locale.Tr "repo.diff.file_suppressed_line_too_long"}} {{else}} {{ctx.Locale.Tr "repo.diff.file_suppressed"}} - <a class="ui basic tiny button diff-load-button" data-href="{{$.Link}}?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{ctx.Locale.Tr "repo.diff.load"}}</a> + <a class="ui basic tiny button diff-load-button" data-href="?file-only=true&files={{$file.Name}}&files={{$file.OldName}}">{{ctx.Locale.Tr "repo.diff.load"}}</a> {{end}} {{else}} {{ctx.Locale.Tr "repo.diff.bin_not_shown"}} @@ -202,8 +207,8 @@ </div> {{if $showFileViewToggle}} {{/* for image or CSV, it can have a horizontal scroll bar, there won't be review comment context menu (position absolute) which would be clipped by "overflow" */}} - <div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} gt-overflow-x-scroll"> - <table class="chroma gt-w-full"> + <div id="diff-rendered-{{$file.NameHash}}" class="file-body file-code {{if $.IsSplitStyle}}code-diff-split{{else}}code-diff-unified{{end}} tw-overflow-x-scroll"> + <table class="chroma tw-w-full"> {{if $isImage}} {{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead "sniffedTypeBase" $sniffedTypeBase "sniffedTypeHead" $sniffedTypeHead}} {{else}} @@ -217,10 +222,10 @@ {{end}} {{if .Diff.IsIncomplete}} - <div class="diff-file-box diff-box file-content gt-mt-3" id="diff-incomplete"> - <h4 class="ui top attached normal header gt-df gt-ac gt-sb"> + <div class="diff-file-box diff-box file-content tw-mt-2" id="diff-incomplete"> + <h4 class="ui top attached header tw-font-normal tw-flex tw-items-center tw-justify-between"> {{ctx.Locale.Tr "repo.diff.too_many_files"}} - <a class="ui basic tiny button" id="diff-show-more-files" data-href="{{$.Link}}?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a> + <a class="ui basic tiny button" id="diff-show-more-files" data-href="?skip-to={{.Diff.End}}&file-only=true">{{ctx.Locale.Tr "repo.diff.show_more"}}</a> </h4> </div> {{end}} @@ -237,6 +242,11 @@ "TextareaName" "content" "DropzoneParentContainer" ".ui.form" )}} + {{if .IsAttachmentEnabled}} + <div class="field"> + {{template "repo/upload" .}} + </div> + {{end}} <div class="text right edit buttons"> <button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> <button class="ui primary save button">{{ctx.Locale.Tr "repo.issues.save"}}</button> @@ -246,5 +256,6 @@ {{end}} {{if (not .DiffNotAvailable)}} {{template "repo/issue/view_content/reference_issue_dialog" .}} + {{template "shared/user/block_user_dialog" .}} {{end}} </div> diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl index 767c2613a0..856b3da01a 100644 --- a/templates/repo/diff/comment_form.tmpl +++ b/templates/repo/diff/comment_form.tmpl @@ -1,5 +1,5 @@ {{if and $.root.SignedUserID (not $.Repository.IsArchived)}} - <form class="ui form {{if $.hidden}}gt-hidden comment-form{{end}}" action="{{$.root.Issue.Link}}/files/reviews/comments" method="post"> + <form class="ui form {{if $.hidden}}tw-hidden comment-form{{end}}" action="{{$.root.Issue.Link}}/files/reviews/comments" method="post"> {{$.root.CsrfTokenHtml}} <input type="hidden" name="origin" value="{{if $.root.PageIsPullFiles}}diff{{else}}timeline{{end}}"> <input type="hidden" name="latest_commit_id" value="{{$.root.AfterCommitID}}"> @@ -19,9 +19,15 @@ "DisableAutosize" "true" )}} - <div class="field footer gt-mx-3"> - <span class="markup-info">{{svg "octicon-markup"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span> - <div class="gt-text-right"> + {{if $.root.IsAttachmentEnabled}} + <div class="field"> + {{template "repo/upload" $.root}} + </div> + {{end}} + + <div class="field footer tw-mx-2"> + <span class="markup-info">{{svg "octicon-markdown"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span> + <div class="tw-text-right"> {{if $.reply}} <button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button> <input type="hidden" name="reply" value="{{$.reply}}"> diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index 2fbfe2fd6a..a9120465bd 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -8,36 +8,36 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} <div class="content comment-container"> - <div class="ui top attached header comment-header gt-df gt-ac gt-sb"> - <div class="comment-header-left gt-df gt-ac"> + <div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between"> + <div class="comment-header-left tw-flex tw-items-center"> {{if .OriginalAuthor}} - <span class="text black gt-font-semibold gt-mr-2"> + <span class="text black tw-font-semibold tw-mr-1"> {{svg (MigrationIcon $.root.Repository.GetOriginalURLHostname)}} {{.OriginalAuthor}} </span> <span class="text grey muted-links"> - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} </span> <span class="text migrate"> {{if $.root.Repository.OriginalURL}} - ({{ctx.Locale.Tr "repo.migrated_from" ($.root.Repository.OriginalURL | Escape) ($.root.Repository.GetOriginalURLHostname | Escape) | Safe}}) + ({{ctx.Locale.Tr "repo.migrated_from" $.root.Repository.OriginalURL $.root.Repository.GetOriginalURLHostname}}) {{end}} </span> {{else}} <span class="text grey muted-links"> {{template "shared/user/namelink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} </span> {{end}} </div> - <div class="comment-header-right actions gt-df gt-ac"> + <div class="comment-header-right actions tw-flex tw-items-center"> {{if .Invalidated}} {{$referenceUrl := printf "%s#%s" $.root.Issue.Link .HashTag}} - <a href="{{AppSubUrl}}{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}"> + <a href="{{$referenceUrl}}" class="ui label basic small" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}"> {{ctx.Locale.Tr "repo.issues.review.outdated"}} </a> {{end}} - {{if and .Review}} + {{if .Review}} {{if eq .Review.Type 0}} <div class="ui label basic small yellow pending-label" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.pending.tooltip" (ctx.Locale.Tr "repo.diff.review") (ctx.Locale.Tr "repo.diff.review.approve") (ctx.Locale.Tr "repo.diff.review.comment") (ctx.Locale.Tr "repo.diff.review.reject")}}"> {{ctx.Locale.Tr "repo.issues.review.pending"}} @@ -55,13 +55,16 @@ <div class="ui attached segment comment-body"> <div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}data-can-edit="true"{{end}}> {{if .RenderedContent}} - {{.RenderedContent|Str2html}} + {{.RenderedContent}} {{else}} <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> {{end}} </div> - <div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div> - <div class="edit-content-zone gt-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}"></div> + <div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div> + <div class="edit-content-zone tw-hidden" data-update-url="{{$.root.RepoLink}}/comments/{{.ID}}" data-context="{{$.root.RepoLink}}" data-attachment-url="{{$.root.RepoLink}}/comments/{{.ID}}/attachments"></div> + {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} + {{end}} </div> {{$reactions := .Reactions.GroupByType}} {{if $reactions}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 15574ad988..d0472577d0 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -11,14 +11,6 @@ {{ctx.Locale.Tr "action.compare_commits_general"}} {{end}} </h2> - {{if .Flash.WarningMsg}} - {{/* - There's already an importing of alert.tmpl in new_form.tmpl, - but only the negative message will be displayed within forms for some reasons, see semantic.css:10659. - To avoid repeated negative messages, the importing here if for .Flash.WarningMsg only. - */}} - {{template "base/alert" .}} - {{end}} {{$BaseCompareName := $.BaseName -}} {{- $HeadCompareName := $.HeadRepo.OwnerName -}} {{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}} @@ -36,8 +28,8 @@ {{- end -}} {{- end -}} <div class="ui segment choose branch"> - <a class="gt-mr-3" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a> - <div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}"> + <a class="tw-mr-2" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a> + <div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}"> <div class="ui basic small button"> <span class="text">{{if $.PageIsComparePull}}{{ctx.Locale.Tr "repo.pulls.compare_base"}}{{else}}{{ctx.Locale.Tr "repo.compare.compare_base"}}{{end}}: {{$BaseCompareName}}:{{$.BaseBranch}}</span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} @@ -52,12 +44,12 @@ <div class="two column row"> <a class="reference column" href="#" data-target=".base-branch-list"> <span class="text black"> - {{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}} + {{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}} </span> </a> <a class="reference column" href="#" data-target=".base-tag-list"> <span class="text black"> - {{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}} + {{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}} </span> </a> </div> @@ -83,7 +75,7 @@ {{end}} {{end}} </div> - <div class="scrolling menu reference-list-menu base-tag-list gt-hidden"> + <div class="scrolling menu reference-list-menu base-tag-list tw-hidden"> {{range .Tags}} <div class="item {{if eq $.BaseBranch .}}selected{{end}}" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments .}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments $.HeadBranch}}">{{$BaseCompareName}}:{{.}}</div> {{end}} @@ -121,12 +113,12 @@ <div class="two column row"> <a class="reference column" href="#" data-target=".head-branch-list"> <span class="text black"> - {{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}} + {{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}} </span> </a> <a class="reference column" href="#" data-target=".head-tag-list"> <span class="text black"> - {{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}} + {{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}} </span> </a> </div> @@ -152,7 +144,7 @@ {{end}} {{end}} </div> - <div class="scrolling menu reference-list-menu head-tag-list gt-hidden"> + <div class="scrolling menu reference-list-menu head-tag-list tw-hidden"> {{range .HeadTags}} <div class="{{if eq $.HeadBranch .}}selected{{end}} item" data-url="{{$.RepoLink}}/compare/{{PathEscapeSegments $.BaseBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.HeadUser.Name}}/{{PathEscape $.HeadRepo.Name}}:{{end}}{{PathEscapeSegments .}}">{{$HeadCompareName}}:{{.}}</div> {{end}} @@ -179,10 +171,10 @@ {{if .IsNothingToCompare}} {{if and $.IsSigned $.AllowEmptyPr (not .Repository.IsArchived) .PageIsComparePull}} <div class="ui segment">{{ctx.Locale.Tr "repo.pulls.nothing_to_compare_and_allow_empty_pr"}}</div> - <div class="ui info message show-form-container {{if .Flash}}gt-hidden{{end}}"> + <div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}"> <button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button> </div> - <div class="pullrequest-form {{if not .Flash}}gt-hidden{{end}}"> + <div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}"> {{template "repo/issue/new_form" .}} </div> {{else if and .HeadIsBranch .BaseIsBranch}} @@ -194,7 +186,7 @@ {{if .HasPullRequest}} <div class="ui segment grid title"> <div class="twelve wide column issue-title"> - {{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}} + {{ctx.Locale.Tr "repo.pulls.has_pull_request" (print $.RepoLink "/pulls/" .PullRequest.Issue.Index) $.RepoRelPath .PullRequest.Index}} <h1> <span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}}</span> <span class="index">#{{.PullRequest.Issue.Index}}</span> @@ -202,30 +194,30 @@ </div> <div class="four wide column middle aligned text right"> {{- if .PullRequest.HasMerged -}} - <a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a> + <a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a> {{else if .Issue.IsClosed}} - <a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a> + <a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a> {{else}} - <a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button primary show-form">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a> + <a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button primary show-form">{{svg "octicon-git-pull-request" 16}} {{ctx.Locale.Tr "repo.pulls.view"}}</a> {{end}} </div> </div> {{else}} {{if and $.IsSigned (not .Repository.IsArchived)}} - <div class="ui info message show-form-container {{if .Flash}}gt-hidden{{end}}"> + <div class="ui info message show-form-container {{if .Flash}}tw-hidden{{end}}"> <button class="ui button primary show-form">{{ctx.Locale.Tr "repo.pulls.new"}}</button> </div> {{else if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} - {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}} + {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}} {{end}} </div> {{end}} {{if $.IsSigned}} - <div class="pullrequest-form {{if not .Flash}}gt-hidden{{end}}"> + <div class="pullrequest-form {{if not .Flash}}tw-hidden{{end}}"> {{template "repo/issue/new_form" .}} </div> {{end}} diff --git a/templates/repo/diff/conversation.tmpl b/templates/repo/diff/conversation.tmpl index feca7b6c0b..c263ddcdd6 100644 --- a/templates/repo/diff/conversation.tmpl +++ b/templates/repo/diff/conversation.tmpl @@ -1,66 +1,72 @@ -{{$resolved := (index .comments 0).IsResolved}} -{{$invalid := (index .comments 0).Invalidated}} -{{$resolveDoer := (index .comments 0).ResolveDoer}} -{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}} -{{$referenceUrl := printf "%s#%s" $.Issue.Link (index .comments 0).HashTag}} -<div class="conversation-holder" data-path="{{(index .comments 0).TreePath}}" data-side="{{if lt (index .comments 0).Line 0}}left{{else}}right{{end}}" data-idx="{{(index .comments 0).UnsignedLine}}"> - {{if $resolved}} - <div class="ui attached header resolved-placeholder gt-df gt-ac gt-sb"> - <div class="ui grey text gt-df gt-ac gt-fw gt-gap-2"> - {{svg "octicon-check" 16 "icon gt-mr-2"}} - <b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}} - {{if $invalid}} - <!-- - We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above). - The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl - --> - <a href="{{AppSubUrl}}{{$referenceUrl}}" class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}"> - {{ctx.Locale.Tr "repo.issues.review.outdated"}} - </a> +{{if len .comments}} + {{$comment := index .comments 0}} + {{$resolved := $comment.IsResolved}} + {{$invalid := $comment.Invalidated}} + {{$resolveDoer := $comment.ResolveDoer}} + {{$hasReview := and $comment.Review}} + {{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}} + {{$referenceUrl := printf "%s#%s" $.Issue.Link $comment.HashTag}} + <div class="conversation-holder" data-path="{{$comment.TreePath}}" data-side="{{if lt $comment.Line 0}}left{{else}}right{{end}}" data-idx="{{$comment.UnsignedLine}}"> + {{if $resolved}} + <div class="ui attached header resolved-placeholder tw-flex tw-items-center tw-justify-between"> + <div class="ui grey text tw-flex tw-items-center tw-flex-wrap tw-gap-1"> + {{svg "octicon-check" 16 "icon tw-mr-1"}} + <b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}} + {{if $invalid}} + <!-- + We only handle the case $resolved=true and $invalid=true in this template because if the comment is not resolved it has the outdated label in the comments area (not the header above). + The case $resolved=false and $invalid=true is handled in repo/diff/comments.tmpl + --> + <a href="{{$referenceUrl}}" class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}"> + {{ctx.Locale.Tr "repo.issues.review.outdated"}} + </a> + {{end}} + </div> + <div class="tw-flex tw-items-center tw-gap-2"> + <button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button show-outdated tw-flex tw-items-center"> + {{svg "octicon-unfold" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.issues.review.show_resolved"}} + </button> + <button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="ui tiny labeled button hide-outdated tw-flex tw-items-center tw-hidden"> + {{svg "octicon-fold" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.issues.review.hide_resolved"}} + </button> + </div> + </div> + {{end}} + <div id="code-comments-{{$comment.ID}}" class="field comment-code-cloud {{if $resolved}}tw-hidden{{end}}"> + <div class="comment-list"> + <ui class="ui comments"> + {{template "repo/diff/comments" dict "root" $ "comments" .comments}} + </ui> + </div> + <div class="tw-flex tw-justify-end tw-items-center tw-flex-wrap tw-mt-2"> + <div class="ui buttons tw-mr-1"> + <button class="ui icon tiny basic button previous-conversation"> + {{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}} + </button> + <button class="ui icon tiny basic button next-conversation"> + {{svg "octicon-arrow-down" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.next"}} + </button> + </div> + {{if and $.CanMarkConversation $hasReview (not $isReviewPending)}} + <button class="ui icon tiny basic button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation"> + {{if $resolved}} + {{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}} + {{end}} + </button> + {{end}} + {{if and $.SignedUserID (not $.Repository.IsArchived)}} + <button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0"> + {{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}} + </button> {{end}} </div> - <div class="gt-df gt-ac gt-gap-3"> - <button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="ui tiny labeled button show-outdated gt-df gt-ac"> - {{svg "octicon-unfold" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.issues.review.show_resolved"}} - </button> - <button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="ui tiny labeled button hide-outdated gt-df gt-ac gt-hidden"> - {{svg "octicon-fold" 16 "gt-mr-3"}} - {{ctx.Locale.Tr "repo.issues.review.hide_resolved"}} - </button> - </div> + {{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $comment.ReviewID "root" $ "comment" $comment}} </div> - {{end}} - <div id="code-comments-{{(index .comments 0).ID}}" class="field comment-code-cloud {{if $resolved}}gt-hidden{{end}}"> - <div class="comment-list"> - <ui class="ui comments"> - {{template "repo/diff/comments" dict "root" $ "comments" .comments}} - </ui> - </div> - <div class="gt-df gt-je gt-ac gt-fw gt-mt-3"> - <div class="ui buttons gt-mr-2"> - <button class="ui icon tiny basic button previous-conversation"> - {{svg "octicon-arrow-up" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.previous"}} - </button> - <button class="ui icon tiny basic button next-conversation"> - {{svg "octicon-arrow-down" 12 "icon"}} {{ctx.Locale.Tr "repo.issues.next"}} - </button> - </div> - {{if and $.CanMarkConversation $isNotPending}} - <button class="ui icon tiny basic button resolve-conversation" data-origin="diff" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation"> - {{if $resolved}} - {{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}} - {{end}} - </button> - {{end}} - {{if and $.SignedUserID (not $.Repository.IsArchived)}} - <button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0"> - {{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}} - </button> - {{end}} - </div> - {{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}} </div> -</div> +{{else}} + {{template "repo/diff/conversation_outdated"}} +{{end}} diff --git a/templates/repo/diff/image_diff.tmpl b/templates/repo/diff/image_diff.tmpl index 02cca784f6..9ad7916398 100644 --- a/templates/repo/diff/image_diff.tmpl +++ b/templates/repo/diff/image_diff.tmpl @@ -7,15 +7,15 @@ data-mime-before="{{.sniffedTypeBase.GetMimeType}}" data-mime-after="{{.sniffedTypeHead.GetMimeType}}" > - <div class="ui secondary pointing tabular top attached borderless menu new-menu"> - <div class="new-menu-inner"> + <overflow-menu class="ui secondary pointing tabular top attached borderless menu"> + <div class="overflow-menu-items tw-justify-center"> <a class="item active" data-tab="diff-side-by-side-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.side_by_side"}}</a> {{if and .blobBase .blobHead}} <a class="item" data-tab="diff-swipe-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.swipe"}}</a> <a class="item" data-tab="diff-overlay-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.overlay"}}</a> {{end}} </div> - </div> + </overflow-menu> <div class="image-diff-tabs is-loading"> <div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side-{{.file.Index}}"> <div class="diff-side-by-side"> diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl index ae7182c930..a2eae007a5 100644 --- a/templates/repo/diff/new_review.tmpl +++ b/templates/repo/diff/new_review.tmpl @@ -1,5 +1,5 @@ <div id="review-box"> - <button class="ui tiny primary button gt-pr-2 gt-df js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}> + <button class="ui tiny primary button tw-pr-1 tw-flex js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}> {{ctx.Locale.Tr "repo.diff.review"}} <span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} @@ -10,8 +10,8 @@ <form class="ui form form-fetch-action" action="{{.Link}}/reviews/submit" method="post"> {{.CsrfTokenHtml}} <input type="hidden" name="commit_id" value="{{.AfterCommitID}}"> - <div class="field gt-df gt-ac"> - <div class="gt-f1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div> + <div class="field tw-flex tw-items-center"> + <div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div> <a class="muted close">{{svg "octicon-x" 16}}</a> </div> <div class="field"> @@ -31,7 +31,7 @@ <div class="divider"></div> {{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}} {{if $showSelfTooltip}} - <span class="gt-dib" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}"> + <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}"> <button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button> </span> {{else}} @@ -39,7 +39,7 @@ {{end}} <button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button> {{if $showSelfTooltip}} - <span class="gt-dib" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}"> + <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}"> <button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button> </span> {{else}} diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl index 3bcb877cc6..09b7b80e41 100644 --- a/templates/repo/diff/options_dropdown.tmpl +++ b/templates/repo/diff/options_dropdown.tmpl @@ -13,17 +13,17 @@ <a class="item" href="{{$.RepoLink}}/commit/{{PathEscape .Commit.ID.String}}.diff" download="{{ShortSha .Commit.ID.String}}.diff">{{ctx.Locale.Tr "repo.diff.download_diff"}}</a> {{end}} <a id="expand-files-btn" class="item">{{ctx.Locale.Tr "repo.pulls.expand_files"}}</a> - <a id="collapse-files-btn"class="item">{{ctx.Locale.Tr "repo.pulls.collapse_files"}}</a> + <a id="collapse-files-btn" class="item">{{ctx.Locale.Tr "repo.pulls.collapse_files"}}</a> {{if .Issue.Index}} {{if .ShowOutdatedComments}} <a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=false"> - <label class="gt-pointer-events-none"> + <label class="tw-pointer-events-none"> {{ctx.Locale.Tr "repo.issues.review.option.hide_outdated_comments"}} </label> </a> {{else}} <a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=true"> - <label class="gt-pointer-events-none"> + <label class="tw-pointer-events-none"> {{ctx.Locale.Tr "repo.issues.review.option.show_outdated_comments"}} </label> </a> diff --git a/templates/repo/diff/section_split.tmpl b/templates/repo/diff/section_split.tmpl index 5b0d982e96..67e2b195de 100644 --- a/templates/repo/diff/section_split.tmpl +++ b/templates/repo/diff/section_split.tmpl @@ -16,19 +16,19 @@ <tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}" data-line-type="{{.GetHTMLDiffLineType}}"> {{if eq .GetType 4}} <td class="lines-num lines-num-old"> - <div class="gt-df"> + <div class="tw-flex"> {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - <button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - <button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $line.GetExpandDirection 2}} - <button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=split&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold"}} </button> {{end}} @@ -44,10 +44,10 @@ {{- $rightDiff := ""}}{{if $match.RightIdx}}{{$rightDiff = $section.GetComputedInlineDiffFor $match ctx.Locale}}{{end}} <td class="lines-num lines-num-old del-code" data-line-num="{{$line.LeftIdx}}"><span rel="diff-{{$file.NameHash}}L{{$line.LeftIdx}}"></span></td> <td class="lines-escape del-code lines-escape-old">{{if $line.LeftIdx}}{{if $leftDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $leftDiff}}"></button>{{end}}{{end}}</td> - <td class="lines-type-marker lines-type-marker-old del-code"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> + <td class="lines-type-marker lines-type-marker-old del-code"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> <td class="lines-code lines-code-old del-code">{{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/* + */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/* */}}{{svg "octicon-plus"}}{{/* */}}</button>{{/* */}}{{end}}{{/* @@ -59,10 +59,10 @@ */}}</td> <td class="lines-num lines-num-new add-code" data-line-num="{{if $match.RightIdx}}{{$match.RightIdx}}{{end}}"><span rel="{{if $match.RightIdx}}diff-{{$file.NameHash}}R{{$match.RightIdx}}{{end}}"></span></td> <td class="lines-escape add-code lines-escape-new">{{if $match.RightIdx}}{{if $rightDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $rightDiff}}"></button>{{end}}{{end}}</td> - <td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="gt-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td> + <td class="lines-type-marker lines-type-marker-new add-code">{{if $match.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$match.GetLineTypeMarker}}"></span>{{end}}</td> <td class="lines-code lines-code-new add-code">{{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} gt-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/* + */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $match.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$match.RightIdx}}">{{/* */}}{{svg "octicon-plus"}}{{/* */}}</button>{{/* */}}{{end}}{{/* @@ -76,10 +76,10 @@ {{$inlineDiff := $section.GetComputedInlineDiffFor $line ctx.Locale}} <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$file.NameHash}}L{{$line.LeftIdx}}{{end}}"></span></td> <td class="lines-escape lines-escape-old">{{if $line.LeftIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td> - <td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td> + <td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td> <td class="lines-code lines-code-old">{{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 2))}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/* + */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-left{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="left" data-idx="{{$line.LeftIdx}}">{{/* */}}{{svg "octicon-plus"}}{{/* */}}</button>{{/* */}}{{end}}{{/* @@ -91,10 +91,10 @@ */}}</td> <td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$file.NameHash}}R{{$line.RightIdx}}{{end}}"></span></td> <td class="lines-escape lines-escape-new">{{if $line.RightIdx}}{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}{{end}}</td> - <td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td> + <td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span>{{end}}</td> <td class="lines-code lines-code-new">{{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles (not (eq .GetType 3))}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/* + */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-right{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="right" data-idx="{{$line.RightIdx}}">{{/* */}}{{svg "octicon-plus"}}{{/* */}}</button>{{/* */}}{{end}}{{/* diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index 2b901411e2..4111159709 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -12,19 +12,19 @@ {{if eq .GetType 4}} {{if $.root.AfterCommitID}} <td colspan="2" class="lines-num"> - <div class="gt-df"> + <div class="tw-flex"> {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 5)}} - <button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=down&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-down"}} </button> {{end}} {{if or (eq $line.GetExpandDirection 3) (eq $line.GetExpandDirection 4)}} - <button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=up&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold-up"}} </button> {{end}} {{if eq $line.GetExpandDirection 2}} - <button class="code-expander-button" data-url="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}" data-query="{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.root.PageIsWiki}}" data-anchor="diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> + <button class="code-expander-button" hx-target="closest tr" hx-get="{{$.root.RepoLink}}/blob_excerpt/{{PathEscape $.root.AfterCommitID}}?{{$line.GetBlobExcerptQuery}}&style=unified&direction=&wiki={{$.root.PageIsWiki}}&anchor=diff-{{$file.NameHash}}K{{$line.SectionInfo.RightIdx}}"> {{svg "octicon-fold"}} </button> {{end}} @@ -44,7 +44,7 @@ <button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button> {{- end -}} </td> - <td class="lines-type-marker"><span class="gt-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> + <td class="lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> {{if eq .GetType 4}} <td class="chroma lines-code blob-hunk">{{/* */}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{/* @@ -52,7 +52,7 @@ {{else}} <td class="chroma lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}">{{/* */}}{{if and $.root.SignedUserID $.root.PageIsPullFiles}}{{/* - */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} gt-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{/* + */}}<button type="button" aria-label="{{ctx.Locale.Tr "repo.diff.comment.add_line_comment"}}" class="ui primary button add-code-comment add-code-comment-{{if $line.RightIdx}}right{{else}}left{{end}}{{if (not $line.CanComment)}} tw-invisible{{end}}" data-side="{{if $line.RightIdx}}right{{else}}left{{end}}" data-idx="{{if $line.RightIdx}}{{$line.RightIdx}}{{else}}{{$line.LeftIdx}}{{end}}">{{/* */}}{{svg "octicon-plus"}}{{/* */}}</button>{{/* */}}{{end}}{{/* diff --git a/templates/repo/diff/stats.tmpl b/templates/repo/diff/stats.tmpl index db468ab6c8..d0dff1bd09 100644 --- a/templates/repo/diff/stats.tmpl +++ b/templates/repo/diff/stats.tmpl @@ -1,5 +1,5 @@ {{Eval .file.Addition "+" .file.Deletion}} -<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}"> +<span class="diff-stats-bar tw-mx-2" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion}}"> {{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}} <div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .file.Addition "/" "(" .file.Addition "+" .file.Deletion "+" 0.0 ")"}}%"></div> </span> diff --git a/templates/repo/diff/whitespace_dropdown.tmpl b/templates/repo/diff/whitespace_dropdown.tmpl index 7bf2ac9aec..c54de165a4 100644 --- a/templates/repo/diff/whitespace_dropdown.tmpl +++ b/templates/repo/diff/whitespace_dropdown.tmpl @@ -2,26 +2,26 @@ {{svg "gitea-whitespace"}} <div class="menu"> <a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=show-all&show-outdated={{$.ShowOutdatedComments}}"> - <label class="gt-pointer-events-none"> - <input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}> + <label class="tw-pointer-events-none"> + <input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}> {{ctx.Locale.Tr "repo.diff.whitespace_show_everything"}} </label> </a> <a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-all&show-outdated={{$.ShowOutdatedComments}}"> - <label class="gt-pointer-events-none"> - <input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}> + <label class="tw-pointer-events-none"> + <input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}> {{ctx.Locale.Tr "repo.diff.whitespace_ignore_all_whitespace"}} </label> </a> <a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-change&show-outdated={{$.ShowOutdatedComments}}"> - <label class="gt-pointer-events-none"> - <input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}> + <label class="tw-pointer-events-none"> + <input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}> {{ctx.Locale.Tr "repo.diff.whitespace_ignore_amount_changes"}} </label> </a> <a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-eol&show-outdated={{$.ShowOutdatedComments}}"> - <label class="gt-pointer-events-none"> - <input class="gt-mr-3 gt-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}> + <label class="tw-pointer-events-none"> + <input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}> {{ctx.Locale.Tr "repo.diff.whitespace_ignore_at_eol"}} </label> </a> diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl index ab2c3c3349..f9c9eef5aa 100644 --- a/templates/repo/editor/cherry_pick.tmpl +++ b/templates/repo/editor/cherry_pick.tmpl @@ -11,11 +11,11 @@ <div class="repo-editor-header"> <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}} - {{$shalink := printf `<a class="ui primary sha label" href="%s">%s</a>` (Escape $shaurl) (ShortSha .SHA)}} + {{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}} {{if eq .CherryPickType "revert"}} - {{ctx.Locale.Tr "repo.editor.revert" $shalink | Str2html}} + {{ctx.Locale.Tr "repo.editor.revert" $shalink}} {{else}} - {{ctx.Locale.Tr "repo.editor.cherry_pick" $shalink | Str2html}} + {{ctx.Locale.Tr "repo.editor.cherry_pick" $shalink}} {{end}} <a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a> <div class="breadcrumb-divider">:</div> diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 34dde576a1..21ef63288f 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -9,7 +9,7 @@ {{ctx.Locale.Tr "repo.editor.commit_changes"}} {{- end}}</h3> <div class="field"> - <input name="commit_summary" placeholder="{{if .PageIsDelete}}{{ctx.Locale.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{ctx.Locale.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.add_tmpl"}}{{else if .PageIsPatch}}{{ctx.Locale.Tr "repo.editor.patch"}}{{else}}{{ctx.Locale.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus> + <input name="commit_summary" maxlength="100" placeholder="{{if .PageIsDelete}}{{ctx.Locale.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{ctx.Locale.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.add_tmpl"}}{{else if .PageIsPatch}}{{ctx.Locale.Tr "repo.editor.patch"}}{{else}}{{ctx.Locale.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus> </div> <div class="field"> <textarea name="commit_message" placeholder="{{ctx.Locale.Tr "repo.editor.commit_message_desc"}}" rows="5">{{.commit_message}}</textarea> @@ -26,7 +26,7 @@ <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}> <label> {{svg "octicon-git-commit"}} - {{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" (.BranchName|Escape) | Safe}} + {{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}} {{if not .CanCommitToBranch.CanCommitToBranch}} <div class="ui visible small warning message"> {{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}} @@ -39,36 +39,36 @@ </label> </div> </div> - {{if not .Repository.IsEmpty}} - <div class="field"> - <div class="ui radio checkbox"> - {{if .CanCreatePullRequest}} - <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> - {{else}} - <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> - {{end}} - <label> - {{svg "octicon-git-pull-request"}} + {{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}} + <div class="field"> + <div class="ui radio checkbox"> {{if .CanCreatePullRequest}} - {{ctx.Locale.Tr "repo.editor.create_new_branch" | Safe}} + <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> {{else}} - {{ctx.Locale.Tr "repo.editor.create_new_branch_np" | Safe}} + <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" button_text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> {{end}} - </label> + <label> + {{svg "octicon-git-pull-request"}} + {{if .CanCreatePullRequest}} + {{ctx.Locale.Tr "repo.editor.create_new_branch"}} + {{else}} + {{ctx.Locale.Tr "repo.editor.create_new_branch_np"}} + {{end}} + </label> + </div> </div> - </div> - <div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}gt-hidden{{end}}"> - <div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}"> - {{svg "octicon-git-branch"}} - <input type="text" name="new_branch_name" value="{{.new_branch_name}}" class="input-contrast gt-mr-2 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}"> - <span class="text-muted js-quick-pull-normalization-info"></span> + <div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}"> + <div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}"> + {{svg "octicon-git-branch"}} + <input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}"> + <span class="text-muted js-quick-pull-normalization-info"></span> + </div> </div> - </div> {{end}} </div> </div> <button id="commit-button" type="submit" class="ui primary button"> {{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}} </button> - <a class="ui button red" href="{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a> + <a class="ui button red" href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel"}}</a> </div> diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index cfc266731b..46f82c47d4 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -15,13 +15,13 @@ {{range $i, $v := .TreeNames}} <div class="breadcrumb-divider">/</div> {{if eq $i $l}} - <input id="file-name" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus> + <input id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus> <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> {{else}} <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> {{end}} {{end}} - <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> + <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> </div> </div> @@ -30,11 +30,11 @@ <a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a> <a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-markup-mode="file">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a> {{if not .IsNewFile}} - <a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}" data-context="{{.BranchLink}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a> + <a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a> {{end}} </div> <div class="ui bottom attached active tab segment" data-tab="write"> - <textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" + <textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}" data-previewable-extensions="{{.PreviewableExtensions}}" @@ -45,7 +45,7 @@ {{ctx.Locale.Tr "loading"}} </div> <div class="ui bottom attached tab segment diff edit-diff" data-tab="diff"> - {{ctx.Locale.Tr "loading"}} + <div class="tw-p-16"></div> </div> </div> {{template "repo/editor/commit_form" .}} diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl index 44c30bd5f9..ff5c09667f 100644 --- a/templates/repo/editor/patch.tmpl +++ b/templates/repo/editor/patch.tmpl @@ -14,16 +14,16 @@ <div class="breadcrumb-divider">:</div> <a class="section" href="{{$.BranchLink}}">{{.BranchName}}</a> <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> - <input type="hidden" id="tree_path" name="tree_path" value="" required> + <input type="hidden" name="tree_path" value="__dummy_for_EditRepoFileForm.TreePath(Required)__"> <input id="file-name" type="hidden" value="diff.patch"> </div> </div> <div class="field"> <div class="ui top attached tabular menu" data-write="write"> - <a class="active item" data-tab="write">{{svg "octicon-code" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a> + <a class="active item" data-tab="write">{{svg "octicon-code" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.editor.new_patch"}}</a> </div> <div class="ui bottom attached active tab segment" data-tab="write"> - <textarea id="edit_area" name="content" class="gt-hidden" data-id="repo-{{.Repository.Name}}-patch" + <textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-patch" data-context="{{.RepoLink}}" data-line-wrap-extensions="{{.LineWrapExtensions}}"> {{.FileContent}}</textarea> diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index d362a5602a..0a7c49dae3 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -13,7 +13,7 @@ {{range $i, $v := .TreeNames}} <div class="breadcrumb-divider">/</div> {{if eq $i $l}} - <input type="text" id="file-name" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus> + <input type="text" id="file-name" maxlength="500" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus> <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> {{else}} <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index c1ec483b77..cb2a5ba1e9 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -6,11 +6,11 @@ <div class="sixteen wide column content"> {{template "base/alert" .}} {{if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} - {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}} + {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}} {{end}} </div> {{end}} @@ -24,7 +24,7 @@ </h4> <div class="ui attached guide table segment empty-repo-guide"> <div class="item"> - <h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository" | Str2html}}</small></h3> + <h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository"}}</small></h3> <div class="repo-button-row"> {{if and .CanWriteCode (not .Repository.IsArchived)}} @@ -37,14 +37,14 @@ </a> {{end}} {{end}} - <div class="ui action small input gt-df gt-f1"> + <div class="clone-panel ui action small input tw-flex-1"> {{template "repo/clone_buttons" .}} </div> </div> </div> {{if not .Repository.IsArchived}} - <div class="divider gt-my-0"></div> + <div class="divider tw-my-0"></div> <div class="item"> <h3>{{ctx.Locale.Tr "repo.create_new_repo_command"}}</h3> diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl index 33f0f87d61..86c613e3a1 100644 --- a/templates/repo/file_info.tmpl +++ b/templates/repo/file_info.tmpl @@ -1,4 +1,4 @@ -<div class="file-info text grey normal gt-mono"> +<div class="file-info text grey normal tw-font-mono"> {{if .FileIsSymlink}} <div class="file-info-entry"> {{ctx.Locale.Tr "repo.symbolic_link"}} @@ -16,7 +16,7 @@ {{end}} {{if .LFSLock}} <div class="file-info-entry ui" data-tooltip-content="{{.LFSLockHint}}"> - {{svg "octicon-lock" 16 "gt-mr-2"}} + {{svg "octicon-lock" 16 "tw-mr-1"}} <a href="{{.LFSLockOwnerHomeLink}}">{{.LFSLockOwner}}</a> </div> {{end}} diff --git a/templates/repo/find/files.tmpl b/templates/repo/find/files.tmpl index eac6ec2011..548ce2f0e8 100644 --- a/templates/repo/find/files.tmpl +++ b/templates/repo/find/files.tmpl @@ -2,10 +2,10 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository"> {{template "repo/header" .}} <div class="ui container"> - <div class="gt-df gt-ac"> + <div class="tw-flex tw-items-center"> <a href="{{$.RepoLink}}">{{.RepoName}}</a> - <span class="gt-mx-3">/</span> - <div class="ui input gt-f1"> + <span class="tw-mx-2">/</span> + <div class="ui input tw-flex-1"> <input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}"> </div> </div> @@ -13,7 +13,7 @@ <tbody> </tbody> </table> - <div id="repo-find-file-no-result" class="ui row center gt-mt-5 gt-hidden"> + <div id="repo-find-file-no-result" class="ui row center tw-mt-8 tw-hidden"> <h3>{{ctx.Locale.Tr "repo.find_file.no_matching"}}</h3> </div> </div> diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl index b27b55c131..412c59b60e 100644 --- a/templates/repo/forks.tmpl +++ b/templates/repo/forks.tmpl @@ -6,8 +6,8 @@ {{ctx.Locale.Tr "repo.forks"}} </h2> {{range .Forks}} - <div class="gt-df gt-ac gt-py-3"> - <span class="gt-mr-2">{{ctx.AvatarUtils.Avatar .Owner}}</span> + <div class="tw-flex tw-items-center tw-py-2"> + <span class="tw-mr-1">{{ctx.AvatarUtils.Avatar .Owner}}</span> <a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a> </div> {{end}} diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl index 37305d278a..9eb4bd4ecb 100644 --- a/templates/repo/graph.tmpl +++ b/templates/repo/graph.tmpl @@ -12,7 +12,7 @@ <div class="menu"> <div class="item" data-value="...flow-hide-pr-refs"> <span class="truncate"> - {{svg "octicon-eye-closed" 16 "gt-mr-2"}}<span title="{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}">{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}</span> + {{svg "octicon-eye-closed" 16 "tw-mr-1"}}<span title="{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}">{{ctx.Locale.Tr "repo.commit_graph.hide_pr_refs"}}</span> </span> </div> {{range .AllRefs}} @@ -20,37 +20,37 @@ {{if eq $refGroup "pull"}} <div class="item" data-value="{{.Name}}"> <span class="truncate"> - {{svg "octicon-git-pull-request" 16 "gt-mr-2"}}<span title="{{.ShortName}}">#{{.ShortName}}</span> + {{svg "octicon-git-pull-request" 16 "tw-mr-1"}}<span title="{{.ShortName}}">#{{.ShortName}}</span> </span> </div> {{else if eq $refGroup "tags"}} <div class="item" data-value="{{.Name}}"> <span class="truncate"> - {{svg "octicon-tag" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span> + {{svg "octicon-tag" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span> </span> </div> {{else if eq $refGroup "remotes"}} <div class="item" data-value="{{.Name}}"> <span class="truncate"> - {{svg "octicon-cross-reference" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span> + {{svg "octicon-cross-reference" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span> </span> </div> {{else if eq $refGroup "heads"}} <div class="item" data-value="{{.Name}}"> <span class="truncate"> - {{svg "octicon-git-branch" 16 "gt-mr-2"}}<span title="{{.ShortName}}">{{.ShortName}}</span> + {{svg "octicon-git-branch" 16 "tw-mr-1"}}<span title="{{.ShortName}}">{{.ShortName}}</span> </span> </div> {{end}} {{end}} </div> </div> - <button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button> - <button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button> + <button id="flow-color-monochrome" class="ui labelled icon button{{if eq .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}">{{svg "material-invert-colors" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.monochrome"}}</button> + <button id="flow-color-colored" class="ui labelled icon button{{if ne .Mode "monochrome"}} active{{end}}" title="{{ctx.Locale.Tr "repo.commit_graph.color"}}">{{svg "material-palette" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.commit_graph.color"}}</button> </div> </h2> <div class="ui dividing"></div> - <div class="ui segment loading gt-hidden" id="loading-indicator"></div> + <div class="is-loading tw-py-32 tw-hidden" id="loading-indicator"></div> {{template "repo/graph/svgcontainer" .}} {{template "repo/graph/commits" .}} </div> diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index 61ef1fe10d..f141dbeada 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -28,10 +28,10 @@ {{- end -}} </a> </span> - <span class="message gt-dib gt-ellipsis gt-mr-3"> + <span class="message tw-inline-block gt-ellipsis tw-mr-2"> <span>{{RenderCommitMessage $.Context $commit.Subject ($.Repository.ComposeMetas ctx)}}</span> </span> - <span class="commit-refs gt-df gt-ac gt-mr-2"> + <span class="commit-refs tw-flex tw-items-center tw-mr-1"> {{range $commit.Refs}} {{$refGroup := .RefGroup}} {{if eq $refGroup "pull"}} @@ -58,20 +58,20 @@ {{end}} {{end}} </span> - <span class="author gt-df gt-ac gt-mr-3"> + <span class="author tw-flex tw-items-center tw-mr-2"> {{$userName := $commit.Commit.Author.Name}} {{if $commit.User}} - {{if $commit.User.FullName}} + {{if and $commit.User.FullName DefaultShowFullName}} {{$userName = $commit.User.FullName}} {{end}} - <span class="gt-mr-2">{{ctx.AvatarUtils.Avatar $commit.User}}</span> + <span class="tw-mr-1">{{ctx.AvatarUtils.Avatar $commit.User}}</span> <a href="{{$commit.User.HomeLink}}">{{$userName}}</a> {{else}} - <span class="gt-mr-2">{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName}}</span> + <span class="tw-mr-1">{{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $userName}}</span> {{$userName}} {{end}} </span> - <span class="time gt-df gt-ac">{{DateTime "full" $commit.Date}}</span> + <span class="time tw-flex tw-items-center">{{DateTime "full" $commit.Date}}</span> {{end}} </li> {{end}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 93102467cc..5e2774dfa1 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -1,31 +1,33 @@ -<div class="header-wrapper"> +<div class="secondary-nav"> {{with .Repository}} <div class="ui container"> <div class="repo-header"> - <div class="flex-item gt-ac"> - <div class="flex-item-leading">{{template "repo/icon" .}}</div> + <div class="flex-item tw-items-center"> + <div class="flex-item-leading"> + {{template "repo/icon" .}} + </div> <div class="flex-item-main"> - <div class="flex-item-title"> - <a class="text light thin" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/ - <a class="text primary" href="{{$.RepoLink}}">{{.Name}}</a></div> + <div class="flex-item-title tw-text-18"> + <a class="muted tw-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a> + </div> </div> <div class="flex-item-trailing"> {{if .IsArchived}} - <span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span> - <div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.archived"}}">{{svg "octicon-archive" 18}}</div> + <span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.archived"}}</span> + <div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.archived"}}">{{svg "octicon-archive" 18}}</div> {{end}} {{if .IsPrivate}} - <span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private"}}</span> - <div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.private"}}">{{svg "octicon-lock" 18}}</div> + <span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.private"}}</span> + <div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.private"}}">{{svg "octicon-lock" 18}}</div> {{else}} {{if .Owner.Visibility.IsPrivate}} - <span class="ui basic label">{{ctx.Locale.Tr "repo.desc.internal"}}</span> - <div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div> + <span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.internal"}}</span> + <div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div> {{end}} {{end}} {{if .IsTemplate}} - <span class="ui basic label">{{ctx.Locale.Tr "repo.desc.template"}}</span> - <div class="repo-icon" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div> + <span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.template"}}</span> + <div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div> {{end}} {{if eq .ObjectFormatName "sha256"}} <span class="ui basic label">{{ctx.Locale.Tr "repo.desc.sha256"}}</span> @@ -76,34 +78,34 @@ <a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button" {{if not $.CanSignedUserFork}} {{if gt (len $.UserAndOrgForks) 1}} - data-modal="#fork-repo-modal" + href="#" data-modal="#fork-repo-modal" {{else if eq (len $.UserAndOrgForks) 1}} href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}" {{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}} {{end}} {{else if not $.UserAndOrgForks}} - href="{{AppSubUrl}}/repo/fork/{{.ID}}" + href="{{$.RepoLink}}/fork" {{else}} - data-modal="#fork-repo-modal" + href="#" data-modal="#fork-repo-modal" {{end}} > - {{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span> + {{svg "octicon-repo-forked"}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.fork"}}</span> </a> <div class="ui small modal" id="fork-repo-modal"> <div class="header"> {{ctx.Locale.Tr "repo.already_forked" .Name}} </div> - <div class="content gt-text-left"> + <div class="content tw-text-left"> <div class="ui list"> {{range $.UserAndOrgForks}} - <div class="ui item gt-py-3"> - <a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a> + <div class="ui item tw-py-2"> + <a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "tw-mr-2"}}{{.FullName}}</a> </div> {{end}} </div> {{if $.CanSignedUserFork}} <div class="divider"></div> - <a href="{{AppSubUrl}}/repo/fork/{{.ID}}">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a> + <a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a> {{end}} </div> </div> @@ -126,9 +128,9 @@ {{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}} </div> {{end}} - <div class="ui container secondary pointing tabular top attached borderless menu new-menu navbar"> + <overflow-menu class="ui container secondary pointing tabular top attached borderless menu tw-pt-0 tw-my-0"> {{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}} - <div class="new-menu-inner"> + <div class="overflow-menu-items"> {{if .Permission.CanRead $.UnitTypeCode}} <a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.BranchNameSubURL}}{{end}}"> {{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}} @@ -174,7 +176,8 @@ </a> {{end}} - {{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}} + {{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}} + {{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}} <a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item"> {{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} {{if .Repository.NumOpenProjects}} @@ -219,12 +222,12 @@ {{end}} </div> {{else if .Permission.IsAdmin}} - <div class="new-menu-inner"> + <div class="overflow-menu-items"> <a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} </a> </div> {{end}} - </div> + </overflow-menu> <div class="ui tabs divider"></div> </div> diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index d91dc4394e..e18a0aec17 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -5,46 +5,34 @@ {{template "base/alert" .}} {{template "repo/code/recently_pushed_new_branches" .}} {{if and (not .HideRepoInfo) (not .IsBlame)}} - <div class="ui repo-description gt-word-break"> - <div id="repo-desc" class="gt-font-16"> + <div class="repo-description"> + <div id="repo-desc" class="gt-word-break tw-text-16"> {{$description := .Repository.DescriptionHTML $.Context}} {{if $description}}<span class="description">{{$description | RenderCodeBlock}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{ctx.Locale.Tr "repo.no_desc"}}</span>{{end}} <a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a> </div> - {{if .RepoSearchEnabled}} - <div class="ui repo-search"> - <form class="ui form ignore-dirty" action="{{.RepoLink}}/search" method="get"> - <div class="field"> - <div class="ui small action input{{if .CodeIndexerUnavailable}} disabled left icon{{end}}"{{if .CodeIndexerUnavailable}} data-tooltip-content="{{ctx.Locale.Tr "repo.search.code_search_unavailable"}}"{{end}}> - <input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}"> - {{if .CodeIndexerUnavailable}} - <i class="icon">{{svg "octicon-alert"}}</i> - {{end}} - <button class="ui small icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit"> - {{svg "octicon-search"}} - </button> - </div> - </div> - </form> + <form class="ignore-dirty" action="{{.RepoLink}}/search" method="get"> + <div class="ui small action input"> + <input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> + {{template "shared/search/button"}} </div> - {{end}} + </form> </div> - <div class="gt-df gt-ac gt-fw gt-gap-2" id="repo-topics"> - {{range .Topics}}<a class="ui repo-topic large label topic gt-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}} - {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg gt-font-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}} + <div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics"> + {{/* it should match the code in issue-home.js */}} + {{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}} + {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}} </div> {{end}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} - <div class="ui form gt-hidden gt-df gt-fc gt-mt-4" id="topic_edit"> - <div class="field gt-f1 gt-mb-2"> - <div class="ui fluid multiple search selection dropdown gt-fw" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}"> - <input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}"> - {{range .Topics}} - {{/* keey the same layout as Fomantic UI generated labels */}} - <a class="ui label transition visible gt-cursor-default gt-dib" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a> - {{end}} - <div class="text"></div> - </div> + <div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit"> + <div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1"> + <input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}"> + {{range .Topics}} + {{/* keep the same layout as Fomantic UI generated labels */}} + <a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a> + {{end}} + <div class="text"></div> </div> <div> <button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button> @@ -53,18 +41,21 @@ </div> {{end}} {{if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{if .Repository.ArchivedUnix.IsZero}} {{ctx.Locale.Tr "repo.archive.title"}} {{else}} - {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix) | Safe}} + {{ctx.Locale.Tr "repo.archive.title_date" (DateTime "long" .Repository.ArchivedUnix)}} {{end}} </div> {{end}} {{template "repo/sub_menu" .}} + {{$n := len .TreeNames}} + {{$l := Eval $n "-" 1}} + {{$isHomepage := (eq $n 0)}} <div class="repo-button-row"> - <div class="gt-df gt-ac gt-fw gt-gap-y-3"> - {{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "gt-mr-2"}} + <div class="tw-flex tw-items-center tw-gap-y-2"> + {{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}} {{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} {{$cmpBranch := ""}} {{if ne .Repository.ID .BaseRepo.ID}} @@ -78,14 +69,12 @@ </a> {{end}} <!-- Show go to file and breadcrumbs if not on home page --> - {{$n := len .TreeNames}} - {{$l := Eval $n "-" 1}} - {{if eq $n 0}} + {{if $isHomepage}} <a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} - <button class="ui dropdown basic compact jump button gt-mr-2"{{if not .Repository.CanEnableEditor}} disabled{{end}}> + <button class="ui dropdown basic compact jump button tw-mr-1"{{if not .Repository.CanEnableEditor}} disabled{{end}}> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> @@ -104,51 +93,53 @@ </button> {{end}} - {{if and (eq $n 0) (.Repository.IsTemplate)}} + {{if and $isHomepage (.Repository.IsTemplate)}} <a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}"> {{ctx.Locale.Tr "repo.use_template"}} </a> {{end}} - {{if ne $n 0}} - <span class="breadcrumb repo-path gt-ml-2"> + {{if (not $isHomepage)}} + <span class="breadcrumb repo-path tw-ml-1"> <a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> {{- range $i, $v := .TreeNames -}} <span class="breadcrumb-divider">/</span> {{- if eq $i $l -}} - <span class="active section" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</span> + <span class="active section" title="{{$v}}">{{$v}}</span> {{- else -}} - {{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</a></span> + {{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{$v}}</a></span> {{- end -}} {{- end -}} </span> {{end}} </div> - <div class="gt-df gt-ac"> + <div class="tw-flex tw-items-center"> <!-- Only show clone panel in repository home page --> - {{if eq $n 0}} - <div class="ui action tiny input" id="clone-panel"> + {{if $isHomepage}} + <div class="clone-panel ui action tiny input"> {{template "repo/clone_buttons" .}} - <button id="more-btn" class="ui basic small compact jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}"> + <button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}"> {{svg "octicon-kebab-horizontal"}} <div class="menu"> {{if not $.DisableDownloadSourceArchives}} - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_zip"}}</a> - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_tar"}}</a> - <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a> - {{if .CitiationExist}} - <a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a> - {{end}} + <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a> + <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a> + <a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a> + {{end}} + {{if .CitiationExist}} + <a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a> + {{end}} + {{range .OpenWithEditorApps}} + <a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a> {{end}} - <a class="item js-clone-url-vsc" href="vscode://vscode.git/clone?url={{.CloneButtonOriginLink.HTTPS}}">{{svg "gitea-vscode" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.clone_in_vsc"}}</a> </div> </button> {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} </div> {{template "repo/cite/cite_modal" .}} {{end}} - {{if and (ne $n 0) (not .IsViewFile) (not .IsBlame)}} + {{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}} <a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}"> - {{svg "octicon-history" 16 "gt-mr-3"}}{{ctx.Locale.Tr "repo.file_history"}} + {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} </a> {{end}} </div> diff --git a/templates/repo/icon.tmpl b/templates/repo/icon.tmpl index 5a80b959d0..e5e0bd68e7 100644 --- a/templates/repo/icon.tmpl +++ b/templates/repo/icon.tmpl @@ -1,10 +1,10 @@ {{$avatarLink := (.RelAvatarLink ctx)}} {{if $avatarLink}} - <img class="ui avatar gt-vm" src="{{$avatarLink}}" width="32" height="32" alt="{{.FullName}}"> + <img class="ui avatar tw-align-middle" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}"> {{else if $.IsMirror}} - {{svg "octicon-mirror" 32}} + {{svg "octicon-mirror" 24}} {{else if $.IsFork}} - {{svg "octicon-repo-forked" 32}} + {{svg "octicon-repo-forked" 24}} {{else}} - {{svg "octicon-repo" 32}} + {{svg "octicon-repo" 24}} {{end}} diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl index 9b7a05ce35..b8ac9a6194 100644 --- a/templates/repo/issue/branch_selector_field.tmpl +++ b/templates/repo/issue/branch_selector_field.tmpl @@ -4,8 +4,8 @@ <form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form"> {{$.CsrfTokenHtml}} </form> - -<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}"> +{{/* TODO: share this branch selector dropdown with the same in repo page */}} +<div class="ui {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}} floating filter select-branch dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}"> <div class="ui basic small button"> <span class="text branch-name">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span> {{if .HasIssuesOrPullsWritePermission}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}{{end}} @@ -18,33 +18,38 @@ <div class="header"> <div class="ui grid"> <div class="two column row"> - <a class="reference column" href="#" data-target="#branch-list"> + <a class="reference column muted" href="#" data-target="#branch-list"> <span class="text black"> - {{svg "octicon-git-branch" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.branches"}} + {{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}} </span> </a> - <a class="reference column" href="#" data-target="#tag-list"> + <a class="reference column muted" href="#" data-target="#tag-list"> <span class="text"> - {{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.tags"}} + {{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}} </span> </a> </div> </div> </div> + <div class="branch-tag-divider"></div> <div id="branch-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}}"> {{if .Reference}} <div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div> {{end}} {{range .Branches}} <div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector">{{.}}</div> + {{else}} + <div class="item">{{ctx.Locale.Tr "no_results_found"}}</div> {{end}} </div> - <div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} gt-hidden"> + <div id="tag-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}} tw-hidden"> {{if .Reference}} <div class="item text small" data-id="" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div> {{end}} {{range .Tags}} <div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div> + {{else}} + <div class="item">{{ctx.Locale.Tr "no_results_found"}}</div> {{end}} </div> </div> diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 14d08fc0ef..bb9340bb2e 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -6,66 +6,66 @@ {{end}} </div> {{end}} - <div class="content gt-p-0 gt-w-full"> - <div class="gt-df gt-items-start"> + <div class="content tw-p-0 tw-w-full"> + <div class="tw-flex tw-items-start"> <div class="issue-card-icon"> {{template "shared/issueicon" .}} </div> <a class="issue-card-title muted issue-title" href="{{.Link}}">{{.Title | RenderEmoji ctx | RenderCodeBlock}}</a> {{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}} - <a role="button" class="issue-card-unpin muted gt-df gt-ac" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}"> + <a role="button" class="issue-card-unpin muted tw-flex tw-items-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}"> {{svg "octicon-x" 16}} </a> {{end}} </div> - <div class="meta gt-my-2"> + <div class="meta tw-my-1"> <span class="text light grey muted-links"> {{if not $.Page.Repository}}{{.Repo.FullName}}{{end}}#{{.Index}} {{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}} {{if .OriginalAuthor}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}} {{else if gt .Poster.ID 0}} - {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}} {{else}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}} {{end}} </span> </div> {{if .MilestoneID}} - <div class="meta gt-my-2"> + <div class="meta tw-my-1"> <a class="milestone" href="{{.Repo.Link}}/milestone/{{.MilestoneID}}"> - {{svg "octicon-milestone" 16 "gt-mr-2 gt-vm"}} - <span class="gt-vm">{{.Milestone.Name}}</span> + {{svg "octicon-milestone" 16 "tw-mr-1 tw-align-middle"}} + <span class="tw-align-middle">{{.Milestone.Name}}</span> </a> </div> {{end}} {{if $.Page.LinkedPRs}} {{range index $.Page.LinkedPRs .ID}} - <div class="meta gt-my-2"> + <div class="meta tw-my-1"> <a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}"> - <span class="gt-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "gt-mr-2 gt-vm"}}</span> - <span class="gt-vm">{{.Title}} <span class="text light grey">#{{.Index}}</span></span> + <span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span> + <span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span> </a> </div> {{end}} {{end}} {{$tasks := .GetTasks}} {{if gt $tasks 0}} - <div class="meta gt-my-2"> - {{svg "octicon-checklist" 16 "gt-mr-2 gt-vm"}} - <span class="gt-vm">{{.GetTasksDone}} / {{$tasks}}</span> + <div class="meta tw-my-1"> + {{svg "octicon-checklist" 16 "tw-mr-1 tw-align-middle"}} + <span class="tw-align-middle">{{.GetTasksDone}} / {{$tasks}}</span> </div> {{end}} </div> {{if or .Labels .Assignees}} - <div class="extra content labels-list gt-p-0 gt-pt-2"> + <div class="extra content labels-list tw-p-0 tw-pt-1"> {{range .Labels}} - <a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx .}}</a> + <a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{RenderLabel ctx ctx.Locale .}}</a> {{end}} <div class="right floated"> {{range .Assignees}} - <a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28 "mini gt-mr-3"}}</a> + <a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28 "mini tw-mr-2"}}</a> {{end}} </div> </div> diff --git a/templates/repo/issue/choose.tmpl b/templates/repo/issue/choose.tmpl index 127b9d7d87..38cf9e485f 100644 --- a/templates/repo/issue/choose.tmpl +++ b/templates/repo/issue/choose.tmpl @@ -3,7 +3,7 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <div class="navbar"> + <div class="issue-navbar"> {{template "repo/issue/navbar" .}} </div> <div class="divider"></div> @@ -11,8 +11,8 @@ <div class="ui attached segment"> <div class="ui two column grid"> <div class="column left aligned"> - <strong>{{.Name | RenderEmojiPlain}}</strong> - <br>{{.About | RenderEmojiPlain}} + <strong>{{.Name}}</strong> + <br>{{.About}} </div> <div class="column right aligned"> <a href="{{$.RepoLink}}/issues/new?template={{.FileName}}{{if $.milestone}}&milestone={{$.milestone}}{{end}}{{if $.project}}&project={{$.project}}{{end}}" class="ui primary button">{{ctx.Locale.Tr "repo.issues.choose.get_started"}}</a> @@ -24,8 +24,8 @@ <div class="ui attached segment"> <div class="ui two column grid"> <div class="column left aligned"> - <strong>{{.Name | RenderEmojiPlain}}</strong> - <br>{{.About | RenderEmojiPlain}} + <strong>{{.Name}}</strong> + <br>{{.About}} </div> <div class="column right aligned"> <a href="{{.URL}}" class="ui primary button">{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues.choose.open_external_link"}}</a> diff --git a/templates/repo/issue/fields/checkboxes.tmpl b/templates/repo/issue/fields/checkboxes.tmpl index 237f2eb5dd..531f401fb7 100644 --- a/templates/repo/issue/fields/checkboxes.tmpl +++ b/templates/repo/issue/fields/checkboxes.tmpl @@ -1,8 +1,8 @@ -<div class="field"> +<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}"> {{template "repo/issue/fields/header" .}} {{range $i, $opt := .item.Attributes.options}} <div class="field inline"> - <div class="ui checkbox gt-mr-0"> + <div class="ui checkbox tw-mr-0 {{if and ($opt.visible) (not (SliceUtils.Contains $opt.visible "form"))}}tw-hidden{{end}}"> <input type="checkbox" name="form-field-{{$.item.ID}}-{{$i}}" {{if $opt.required}}required{{end}}> <label>{{RenderMarkdownToHtml $.context $opt.label}}</label> </div> diff --git a/templates/repo/issue/fields/dropdown.tmpl b/templates/repo/issue/fields/dropdown.tmpl index 23aa373cd2..f4fa79738c 100644 --- a/templates/repo/issue/fields/dropdown.tmpl +++ b/templates/repo/issue/fields/dropdown.tmpl @@ -1,4 +1,4 @@ -<div class="field"> +<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}"> {{template "repo/issue/fields/header" .}} {{/* FIXME: required validation */}} <div class="ui fluid selection dropdown {{if .item.Attributes.multiple}}multiple clearable{{end}}"> diff --git a/templates/repo/issue/fields/input.tmpl b/templates/repo/issue/fields/input.tmpl index 3fc8a86510..039f9a9f34 100644 --- a/templates/repo/issue/fields/input.tmpl +++ b/templates/repo/issue/fields/input.tmpl @@ -1,4 +1,4 @@ -<div class="field"> +<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}"> {{template "repo/issue/fields/header" .}} <input type="{{if .item.Validations.is_number}}number{{else}}text{{end}}" name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" value="{{.item.Attributes.value}}" {{if .item.Validations.required}}required{{end}} {{if .item.Validations.regex}}pattern="{{.item.Validations.regex}}" title="{{.item.Validations.regex}}"{{end}}> </div> diff --git a/templates/repo/issue/fields/markdown.tmpl b/templates/repo/issue/fields/markdown.tmpl index fd5b6afd22..934699ed05 100644 --- a/templates/repo/issue/fields/markdown.tmpl +++ b/templates/repo/issue/fields/markdown.tmpl @@ -1,3 +1,3 @@ -<div class="field"> +<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}"> <div>{{RenderMarkdownToHtml .Context .item.Attributes.value}}</div> </div> diff --git a/templates/repo/issue/fields/textarea.tmpl b/templates/repo/issue/fields/textarea.tmpl index 55adeb28d0..3ad69e1220 100644 --- a/templates/repo/issue/fields/textarea.tmpl +++ b/templates/repo/issue/fields/textarea.tmpl @@ -1,5 +1,5 @@ {{$useMarkdownEditor := not .item.Attributes.render}} -<div class="field {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}"> +<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}} {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}"> {{template "repo/issue/fields/header" .}} {{/* the real form element to provide the value */}} @@ -7,7 +7,7 @@ {{if $useMarkdownEditor}} {{template "shared/combomarkdowneditor" (dict - "ContainerClasses" "gt-hidden" + "ContainerClasses" "tw-hidden" "MarkdownPreviewUrl" (print .root.RepoLink "/markup") "MarkdownPreviewContext" .root.RepoLink "TextareaContent" .item.Attributes.value @@ -16,7 +16,7 @@ )}} {{if .root.IsAttachmentEnabled}} - <div class="gt-mt-4 form-field-dropzone gt-hidden"> + <div class="tw-mt-4 form-field-dropzone tw-hidden"> {{template "repo/upload" .root}} </div> {{end}} diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index a2296f6597..f23ca36d78 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -29,8 +29,8 @@ <div class="divider"></div> {{end}} {{$previousExclusiveScope = $exclusiveScope}} - <div class="item issue-action gt-df gt-sb" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels"> - {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context .}} + <div class="item issue-action tw-flex tw-justify-between" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/labels"> + {{if SliceUtils.Contains $.SelLabelIDs .ID}}{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}{{end}} {{RenderLabel $.Context ctx.Locale .}} {{template "repo/issue/labels/label_archived" .}} </div> {{end}} @@ -85,7 +85,7 @@ </div> {{range .OpenProjects}} <div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/projects"> - {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </div> {{end}} {{end}} @@ -96,7 +96,7 @@ </div> {{range .ClosedProjects}} <div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.RepoLink}}/issues/projects"> - {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </div> {{end}} {{end}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 511ef7f397..c6de4977dc 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -16,15 +16,15 @@ > <label for="archived-filter-checkbox"> {{ctx.Locale.Tr "repo.issues.label_archived_filter"}} - <i class="gt-ml-2" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}> + <i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}> {{svg "octicon-info"}} </i> </label> </div> - <span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude" | Safe}}</span> + <span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span> <div class="divider"></div> - <a class="{{if .AllLabels}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a> - <a class="{{if .NoLabel}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a> + <a class="{{if .AllLabels}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a> + <a class="{{if .NoLabel}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels=0&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a> {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} @@ -32,7 +32,7 @@ <div class="divider"></div> {{end}} {{$previousExclusiveScope = $exclusiveScope}} - <a class="item label-filter-item gt-df gt-ac" {{if .IsArchived}}data-is-archived{{end}} href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}"> + <a class="item label-filter-item tw-flex tw-items-center" {{if .IsArchived}}data-is-archived{{end}} href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.QueryString}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}" data-label-id="{{.ID}}"> {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} @@ -42,8 +42,8 @@ {{svg "octicon-check"}} {{end}} {{end}} - {{RenderLabel $.Context .}} - <p class="gt-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p> + {{RenderLabel $.Context ctx.Locale .}} + <p class="tw-ml-auto">{{template "repo/issue/labels/label_archived" .}}</p> </a> {{end}} </div> @@ -62,13 +62,13 @@ <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}"> </div> <div class="divider"></div> - <a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a> - <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a> + <a class="{{if not $.MilestoneID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=0&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a> + <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone=-1&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a> {{if .OpenMilestones}} <div class="divider"></div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div> {{range .OpenMilestones}} - <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} </a> @@ -78,7 +78,7 @@ <div class="divider"></div> <div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div> {{range .ClosedMilestones}} - <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID .ID}}active selected {{end}}{{end}}item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{.ID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} </a> @@ -99,16 +99,16 @@ <i class="icon">{{svg "octicon-search" 16}}</i> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_project"}}"> </div> - <a class="{{if not .ProjectID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a> - <a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&project=-1&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a> + <a class="{{if not .ProjectID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_all"}}</a> + <a class="{{if eq .ProjectID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&project=-1&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_project_none"}}</a> {{if .OpenProjects}} <div class="divider"></div> <div class="header"> {{ctx.Locale.Tr "repo.issues.new.open_projects"}} </div> {{range .OpenProjects}} - <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item gt-df" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> - {{svg .IconName 18 "gt-mr-3 gt-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span> + <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + {{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}<span class="gt-ellipsis">{{.Title}}</span> </a> {{end}} {{end}} @@ -118,8 +118,8 @@ {{ctx.Locale.Tr "repo.issues.new.closed_projects"}} </div> {{range .ClosedProjects}} - <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> - {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + <a class="{{if $.ProjectID}}{{if eq $.ProjectID .ID}}active selected{{end}}{{end}} item" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{.ID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </a> {{end}} {{end}} @@ -130,7 +130,7 @@ <div class="ui dropdown jump item user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.author_search_tooltip"}}" data-search-url="{{if .Milestone}}{{$.RepoLink}}/issues/posters{{else}}{{$.Link}}/posters{{end}}" data-selected-user-id="{{$.PosterID}}" - data-action-jump-url="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={user_id}{{if $.ShowArchivedLabels}}&archived=true{{end}}" + data-action-jump-url="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={user_id}{{if $.ShowArchivedLabels}}&archived=true{{end}}" > <span class="text"> {{ctx.Locale.Tr "repo.issues.filter_poster"}} @@ -156,11 +156,11 @@ <i class="icon">{{svg "octicon-search" 16}}</i> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignee"}}"> </div> - <a class="{{if not .AssigneeID}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a> - <a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a> + <a class="{{if not .AssigneeID}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}}</a> + <a class="{{if eq .AssigneeID -1}}active selected {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee=-1&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}</a> <div class="divider"></div> {{range .Assignees}} - <a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item gt-df" href="{{$.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> + <a class="{{if eq $.AssigneeID .ID}}active selected{{end}} item tw-flex" href="?type={{$.ViewType}}&sort={{$.SortType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{.ID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}"> {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} </a> {{end}} @@ -175,14 +175,14 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if eq .ViewType "all"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a> - <a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a> - <a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a> + <a class="{{if eq .ViewType "all"}}active {{end}}item" href="?q={{$.Keyword}}&type=all&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a> + <a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="?q={{$.Keyword}}&type=assigned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a> + <a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=created_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a> {{if .PageIsPullList}} - <a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a> - <a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=reviewed_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a> + <a class="{{if eq .ViewType "review_requested"}}active {{end}}item" href="?q={{$.Keyword}}&type=review_requested&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}</a> + <a class="{{if eq .ViewType "reviewed_by"}}active {{end}}item" href="?q={{$.Keyword}}&type=reviewed_by&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}</a> {{end}} - <a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a> + <a class="{{if eq .ViewType "mentioned"}}active {{end}}item" href="?q={{$.Keyword}}&type=mentioned&sort={{$.SortType}}&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}</a> </div> </div> {{end}} @@ -194,13 +194,13 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> - <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> - <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> - <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> + <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=latest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=oldest&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> + <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> + <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> + <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?q={{$.Keyword}}&type={{$.ViewType}}&sort=farduedate&state={{$.State}}&labels={{.SelectLabels}}&milestone={{$.MilestoneID}}&project={{$.ProjectID}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> </div> </div> diff --git a/templates/repo/issue/filters.tmpl b/templates/repo/issue/filters.tmpl index 56c65e2401..06e7c1aa6c 100644 --- a/templates/repo/issue/filters.tmpl +++ b/templates/repo/issue/filters.tmpl @@ -1,7 +1,7 @@ <div id="issue-filters" class="issue-list-toolbar"> <div class="issue-list-toolbar-left"> {{if and $.CanWriteIssuesOrPulls .Issues}} - <input type="checkbox" autocomplete="off" class="issue-checkbox-all gt-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}"> + <input type="checkbox" autocomplete="off" class="issue-checkbox-all tw-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}"> {{end}} {{template "repo/issue/openclose" .}} <!-- Total Tracked Time --> diff --git a/templates/repo/issue/label_precolors.tmpl b/templates/repo/issue/label_precolors.tmpl index 146119b978..80007662c0 100644 --- a/templates/repo/issue/label_precolors.tmpl +++ b/templates/repo/issue/label_precolors.tmpl @@ -1,5 +1,5 @@ <div class="precolors"> - <div class="gt-df"> + <div class="tw-flex"> <a class="color" style="background-color:#e11d21" data-color-hex="#e11d21"></a> <a class="color" style="background-color:#eb6420" data-color-hex="#eb6420"></a> <a class="color" style="background-color:#fbca04" data-color-hex="#fbca04"></a> @@ -9,7 +9,7 @@ <a class="color" style="background-color:#0052cc" data-color-hex="#0052cc"></a> <a class="color" style="background-color:#5319e7" data-color-hex="#5319e7"></a> </div> - <div class="gt-df"> + <div class="tw-flex"> <a class="color" style="background-color:#f6c6c7" data-color-hex="#f6c6c7"></a> <a class="color" style="background-color:#fad8c7" data-color-hex="#fad8c7"></a> <a class="color" style="background-color:#fef2c0" data-color-hex="#fef2c0"></a> diff --git a/templates/repo/issue/labels.tmpl b/templates/repo/issue/labels.tmpl index 86e4bae0f7..230777efcc 100644 --- a/templates/repo/issue/labels.tmpl +++ b/templates/repo/issue/labels.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository labels"> {{template "repo/header" .}} <div class="ui container"> - <div class="navbar gt-mb-4"> + <div class="issue-navbar tw-mb-4"> {{template "repo/issue/navbar" .}} {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} <button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button> diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl index f41b4ee2c6..fcf69217ea 100644 --- a/templates/repo/issue/labels/edit_delete_label.tmpl +++ b/templates/repo/issue/labels/edit_delete_label.tmpl @@ -29,9 +29,9 @@ <label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label> </div> <br> - <small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}}</small> - <div class="desc gt-ml-2 gt-mt-3 gt-hidden label-exclusive-warning"> - {{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning" | Safe}} + <small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small> + <div class="desc tw-ml-1 tw-mt-2 tw-hidden label-exclusive-warning"> + {{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}} </div> <br> </div> @@ -40,7 +40,7 @@ <input class="label-is-archived-input" name="is_archived" type="checkbox"> <label>{{ctx.Locale.Tr "repo.issues.label_archive"}}</label> </div> - <i class="gt-ml-2" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}> + <i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}> {{svg "octicon-info"}} </i> </div> @@ -52,8 +52,8 @@ </div> <div class="field color-field"> <label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label> - <div class="color picker column"> - <input class="color-picker" name="color" value="#70c24a" required maxlength="7"> + <div class="column js-color-picker-input"> + <input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7"> {{template "repo/issue/label_precolors"}} </div> </div> diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl index 3ecae09373..3651ba118f 100644 --- a/templates/repo/issue/labels/label.tmpl +++ b/templates/repo/issue/labels/label.tmpl @@ -1,7 +1,7 @@ <a - class="item {{if not .label.IsChecked}}gt-hidden{{end}}" + class="item {{if not .label.IsChecked}}tw-hidden{{end}}" id="label_{{.label.ID}}" href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}} > - {{- RenderLabel $.Context .label -}} + {{- RenderLabel $.Context ctx.Locale .label -}} </a> diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index 9a6065a407..8d7fc2c3db 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -3,16 +3,16 @@ <div class="ui right"> <div class="ui secondary menu"> <!-- Sort --> - <div class="item ui jump dropdown gt-py-3"> + <div class="item ui jump dropdown tw-py-2"> <span class="text"> {{ctx.Locale.Tr "repo.issues.filter_sort"}} </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="left menu"> - <a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> - <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="{{$.Link}}?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> - <a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a> - <a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?sort=mostissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a> + <div class="menu"> + <a class="{{if or (eq .SortType "alphabetically") (not .SortType)}}active {{end}}item" href="?sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a> + <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a> + <a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a> + <a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a> </div> </div> </div> @@ -32,7 +32,7 @@ {{range .Labels}} <li class="item"> <div class="label-title"> - {{RenderLabel $.Context .}} + {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}} </div> <div class="label-issues"> @@ -42,9 +42,9 @@ <a class="open-issues" href="{{$.RepoLink}}/issues?labels={{.ID}}">{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.label_open_issues" .NumOpenIssues}}</a> {{end}} </div> - <div class="label-operation gt-df"> + <div class="label-operation tw-flex"> {{template "repo/issue/labels/label_archived" .}} - <div class="gt-df gt-ml-auto"> + <div class="tw-flex tw-ml-auto"> {{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}} <a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a> <a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a> @@ -61,7 +61,7 @@ <li class="item"> <div class="ui grid middle aligned"> <div class="ten wide column"> - {{ctx.Locale.Tr "repo.org_labels_desc" | Str2html}} + {{ctx.Locale.Tr "repo.org_labels_desc"}} {{if .IsOrganizationOwner}} <a href="{{.OrganizationLink}}/settings/labels">({{ctx.Locale.Tr "repo.org_labels_desc_manage"}})</a>: {{end}} @@ -72,7 +72,7 @@ {{range .OrgLabels}} <li class="item org-label"> <div class="label-title"> - {{RenderLabel $.Context .}} + {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}} </div> <div class="label-issues"> diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl index e7fb1e5ff6..32fd8e76d7 100644 --- a/templates/repo/issue/labels/label_new.tmpl +++ b/templates/repo/issue/labels/label_new.tmpl @@ -17,7 +17,7 @@ <label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label> </div> <br> - <small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc" | Safe}}</small> + <small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small> </div> <div class="field"> <label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label> @@ -27,8 +27,8 @@ </div> <div class="field color-field"> <label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label> - <div class="color picker column"> - <input class="color-picker" name="color" value="#70c24a" required maxlength="7"> + <div class="js-color-picker-input column"> + <input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7"> {{template "repo/issue/label_precolors"}} </div> </div> diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl index d24dac46eb..e5f15caca5 100644 --- a/templates/repo/issue/labels/labels_selector_field.tmpl +++ b/templates/repo/issue/labels/labels_selector_field.tmpl @@ -2,7 +2,7 @@ <span class="text muted flex-text-block"> <strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </span> <div class="filter menu" {{if .Issue}}data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels"{{else}}data-id="#label_ids"{{end}}> @@ -21,7 +21,7 @@ <div class="divider"></div> {{end}} {{$previousExclusiveScope = $exclusiveScope}} - <a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}gt-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span> {{RenderLabel $.Context .}} + <a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span> {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}} <p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p> </a> @@ -34,7 +34,7 @@ <div class="divider"></div> {{end}} {{$previousExclusiveScope = $exclusiveScope}} - <a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}gt-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span> {{RenderLabel $.Context .}} + <a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{if $exclusiveScope}}{{svg "octicon-dot-fill"}}{{else}}{{svg "octicon-check"}}{{end}}</span> {{RenderLabel $.Context ctx.Locale .}} {{if .Description}}<br><small class="desc">{{.Description | RenderEmoji $.Context}}</small>{{end}} <p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p> </a> diff --git a/templates/repo/issue/labels/labels_sidebar.tmpl b/templates/repo/issue/labels/labels_sidebar.tmpl index 4f41054a91..be30baba92 100644 --- a/templates/repo/issue/labels/labels_sidebar.tmpl +++ b/templates/repo/issue/labels/labels_sidebar.tmpl @@ -1,5 +1,5 @@ <div class="ui labels list"> - <span class="no-select item {{if .root.HasSelectedLabel}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span> + <span class="no-select item {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span> <span class="labels-list"> {{range .root.Labels}} {{template "repo/issue/labels/label" dict "root" $.root "label" .}} diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 012b613fbf..30edf825f1 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -2,11 +2,12 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository issue-list"> {{template "repo/header" .}} <div class="ui container"> + {{template "base/alert" .}} {{if .PinnedIssues}} <div id="issue-pins" {{if .IsRepoAdmin}}data-is-repo-admin{{end}}> {{range .PinnedIssues}} - <div class="issue-card gt-word-break {{if $.IsRepoAdmin}}gt-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}"> + <div class="issue-card gt-word-break {{if $.IsRepoAdmin}}tw-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}"> {{template "repo/issue/card" (dict "Issue" . "Page" $ "isPinnedIssueCard" true)}} </div> {{end}} @@ -31,7 +32,7 @@ {{template "repo/issue/filters" .}} - <div id="issue-actions" class="issue-list-toolbar gt-hidden"> + <div id="issue-actions" class="issue-list-toolbar tw-hidden"> <div class="issue-list-toolbar-left"> {{template "repo/issue/openclose" .}} <!-- Total Tracked Time --> diff --git a/templates/repo/issue/milestone/filter_list.tmpl b/templates/repo/issue/milestone/filter_list.tmpl index 0eea42d6ee..45f9866a16 100644 --- a/templates/repo/issue/milestone/filter_list.tmpl +++ b/templates/repo/issue/milestone/filter_list.tmpl @@ -5,11 +5,11 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a> - <a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a> - <a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="{{$.Link}}?sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a> - <a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="{{$.Link}}?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a> - <a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a> - <a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a> + <a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a> + <a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a> + <a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a> + <a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a> + <a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a> + <a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a> </div> </div> diff --git a/templates/repo/issue/milestone/select_menu.tmpl b/templates/repo/issue/milestone/select_menu.tmpl index 6f8c6c85c2..9b0492ce52 100644 --- a/templates/repo/issue/milestone/select_menu.tmpl +++ b/templates/repo/issue/milestone/select_menu.tmpl @@ -18,7 +18,7 @@ </div> {{range .OpenMilestones}} <a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}"> - {{svg "octicon-milestone" 16 "gt-mr-2"}} + {{svg "octicon-milestone" 16 "tw-mr-1"}} {{.Name}} </a> {{end}} @@ -30,7 +30,7 @@ </div> {{range .ClosedMilestones}} <a class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?milestone={{.ID}}"> - {{svg "octicon-milestone" 16 "gt-mr-2"}} + {{svg "octicon-milestone" 16 "tw-mr-1"}} {{.Name}} </a> {{end}} diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl index ea19518efa..5bae6fc6d5 100644 --- a/templates/repo/issue/milestone_issues.tmpl +++ b/templates/repo/issue/milestone_issues.tmpl @@ -2,10 +2,11 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository milestone-issue-list"> {{template "repo/header" .}} <div class="ui container"> - <div class="gt-df"> - <h1 class="gt-mb-3">{{.Milestone.Name}}</h1> + {{template "base/alert" .}} + <div class="tw-flex"> + <h1 class="tw-mb-2">{{.Milestone.Name}}</h1> {{if not .Repository.IsArchived}} - <div class="text right gt-f1"> + <div class="text right tw-flex-1"> {{if or .CanWriteIssues .CanWritePulls}} {{if .Milestone.IsClosed}} <a class="ui primary basic button link-action" href data-url="{{$.RepoLink}}/milestones/{{.MilestoneID}}/open">{{ctx.Locale.Tr "repo.milestones.open"}} @@ -21,17 +22,17 @@ {{end}} </div> {{if .Milestone.RenderedContent}} - <div class="markup content gt-mb-4"> - {{.Milestone.RenderedContent|Str2html}} + <div class="markup content tw-mb-4"> + {{.Milestone.RenderedContent}} </div> {{end}} - <div class="gt-df gt-fc gt-gap-3"> + <div class="tw-flex tw-flex-col tw-gap-2"> <progress class="milestone-progress-big" value="{{.Milestone.Completeness}}" max="100"></progress> - <div class="gt-df gt-gap-4"> - <div classs="gt-df gt-ac"> + <div class="tw-flex tw-gap-4"> + <div classs="tw-flex tw-items-center"> {{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}} {{if .IsClosed}} - {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}} + {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}} {{else}} {{if .Milestone.DeadlineString}} @@ -45,7 +46,7 @@ {{end}} {{end}} </div> - <div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}</div> + <div class="tw-mr-2">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}</div> {{if .TotalTrackedTime}} <div data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'> {{svg "octicon-clock"}} diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl index 3e79ee7ee9..9f32df00e3 100644 --- a/templates/repo/issue/milestone_new.tmpl +++ b/templates/repo/issue/milestone_new.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository new milestone"> {{template "repo/header" .}} <div class="ui container"> - <div class="navbar"> + <div class="issue-navbar"> {{template "repo/issue/navbar" .}} {{if and (or .CanWriteIssues .CanWritePulls) .PageIsEditMilestone}} <div class="ui right floated secondary menu"> @@ -39,7 +39,7 @@ <textarea name="content">{{.content}}</textarea> </div> <div class="divider"></div> - <div class="gt-text-right"> + <div class="tw-text-right"> {{if .PageIsEditMilestone}} <a class="ui primary basic button" href="{{.RepoLink}}/milestones"> {{ctx.Locale.Tr "repo.milestones.cancel"}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index 3d4bbfd8b1..bce7ad8717 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -19,12 +19,12 @@ {{range .Milestones}} <li class="milestone-card"> <div class="milestone-header"> - <h3 class="flex-text-block gt-m-0"> + <h3 class="flex-text-block tw-m-0"> {{svg "octicon-milestone" 16}} <a class="muted" href="{{$.RepoLink}}/milestone/{{.ID}}">{{.Name}}</a> </h3> - <div class="gt-df gt-ac"> - <span class="gt-mr-3">{{.Completeness}}%</span> + <div class="tw-flex tw-items-center"> + <span class="tw-mr-2">{{.Completeness}}%</span> <progress value="{{.Completeness}}" max="100"></progress> </div> </div> @@ -47,14 +47,14 @@ {{if .UpdatedUnix}} <div class="flex-text-block"> {{svg "octicon-clock"}} - {{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale) | Safe}} + {{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale)}} </div> {{end}} <div class="flex-text-block"> {{if .IsClosed}} {{$closedDate:= TimeSinceUnix .ClosedDateUnix ctx.Locale}} {{svg "octicon-clock" 14}} - {{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}} + {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}} {{else}} {{if .DeadlineString}} <span class="flex-text-inline {{if .IsOverdue}}text red{{end}}"> @@ -82,7 +82,7 @@ </div> {{if .Content}} <div class="markup content"> - {{.RenderedContent|Str2html}} + {{.RenderedContent}} </div> {{end}} </li> diff --git a/templates/repo/issue/navbar.tmpl b/templates/repo/issue/navbar.tmpl index 16110597ed..30e42c77cc 100644 --- a/templates/repo/issue/navbar.tmpl +++ b/templates/repo/issue/navbar.tmpl @@ -1,4 +1,4 @@ -<h2 class="ui compact small menu header small-menu-items issue-list-navbar"> +<h2 class="ui compact small menu small-menu-items issue-list-navbar"> <a class="{{if .PageIsLabels}}active {{end}}item" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a> <a class="{{if .PageIsMilestones}}active {{end}}item" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a> </h2> diff --git a/templates/repo/issue/new.tmpl b/templates/repo/issue/new.tmpl index 780e874bc6..ccd45fdebe 100644 --- a/templates/repo/issue/new.tmpl +++ b/templates/repo/issue/new.tmpl @@ -2,14 +2,6 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository new issue"> {{template "repo/header" .}} <div class="ui container"> - {{if .Flash.WarningMsg}} - {{/* - There's already an importing of alert.tmpl in new_form.tmpl, - but only the negative message will be displayed within forms for some reasons, see semantic.css:10659. - To avoid repeated negative messages, the importing here if for .Flash.WarningMsg only. - */}} - {{template "base/alert" .}} - {{end}} {{template "repo/issue/new_form" .}} </div> </div> diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 04ae8456bb..88a6c39e52 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -1,19 +1,17 @@ +{{if .Flash}} +{{template "base/alert" .}} +{{end}} <form class="issue-content ui comment form form-fetch-action" id="new-issue" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - {{if .Flash}} - <div class="sixteen wide column"> - {{template "base/alert" .}} - </div> - {{end}} <div class="issue-content-left"> <div class="ui comments"> <div class="comment"> {{ctx.AvatarUtils.Avatar .SignedUser 40}} - <div class="ui segment content gt-my-0"> + <div class="ui segment content tw-my-0"> <div class="field"> - <input name="title" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" autofocus required maxlength="255" autocomplete="off"> + <input name="title" class="js-autofocus-end" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" required maxlength="255" autocomplete="off"> {{if .PageIsComparePull}} - <div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div> + <div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}</div> {{end}} </div> {{if .Fields}} @@ -62,7 +60,7 @@ <span class="text flex-text-block"> <strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if .HasIssuesOrPullsWritePermission}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </span> <div class="menu"> @@ -70,11 +68,11 @@ </div> </div> <div class="ui select-milestone list"> - <span class="no-select item {{if .Milestone}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span> + <span class="no-select item {{if .Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span> <div class="selected"> {{if .Milestone}} <a class="item muted sidebar-item-link" href="{{.RepoLink}}/issues?milestone={{.Milestone.ID}}"> - {{svg "octicon-milestone" 18 "gt-mr-3"}} + {{svg "octicon-milestone" 18 "tw-mr-2"}} {{.Milestone.Name}} </a> {{end}} @@ -89,7 +87,7 @@ <span class="text flex-text-block"> <strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if .HasIssuesOrPullsWritePermission}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </span> <div class="menu"> @@ -112,7 +110,7 @@ </div> {{range .OpenProjects}} <a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}"> - {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </a> {{end}} {{end}} @@ -123,7 +121,7 @@ </div> {{range .ClosedProjects}} <a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}"> - {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </a> {{end}} {{end}} @@ -131,11 +129,11 @@ </div> </div> <div class="ui select-project list"> - <span class="no-select item {{if .Project}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span> + <span class="no-select item {{if .Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span> <div class="selected"> {{if .Project}} <a class="item muted sidebar-item-link" href="{{.Project.Link ctx}}"> - {{svg .Project.IconName 18 "gt-mr-3"}}{{.Project.Title}} + {{svg .Project.IconName 18 "tw-mr-2"}}{{.Project.Title}} </a> {{end}} </div> @@ -147,7 +145,7 @@ <span class="text flex-text-block"> <strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong> {{if .HasIssuesOrPullsWritePermission}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </span> <div class="filter menu" data-id="#assignee_ids"> @@ -158,22 +156,22 @@ <div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div> {{range .Assignees}} <a class="item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}"> - <span class="octicon-check gt-invisible">{{svg "octicon-check"}}</span> + <span class="octicon-check tw-invisible">{{svg "octicon-check"}}</span> <span class="text"> - {{ctx.AvatarUtils.Avatar . 28 "gt-mr-3"}}{{template "repo/search_name" .}} + {{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{template "repo/search_name" .}} </span> </a> {{end}} </div> </div> <div class="ui assignees list"> - <span class="no-select item {{if .HasSelectedLabel}}gt-hidden{{end}}"> + <span class="no-select item {{if .HasSelectedLabel}}tw-hidden{{end}}"> {{ctx.Locale.Tr "repo.issues.new.no_assignees"}} </span> <div class="selected"> {{range .Assignees}} - <a class="item gt-p-2 muted gt-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}"> - {{ctx.AvatarUtils.Avatar . 28 "gt-mr-3 gt-vm"}}{{.GetDisplayName}} + <a class="item tw-p-1 muted tw-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}"> + {{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}} </a> {{end}} </div> diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl index 38848c51ac..eb2d6e09ee 100644 --- a/templates/repo/issue/openclose.tmpl +++ b/templates/repo/issue/openclose.tmpl @@ -1,16 +1,16 @@ <div class="small-menu-items ui compact tiny menu"> <a class="{{if eq .State "open"}}active {{end}}item" href="{{if eq .State "open"}}{{.AllStatesLink}}{{else}}{{.OpenLink}}{{end}}"> {{if .PageIsMilestones}} - {{svg "octicon-milestone" 16 "gt-mr-3"}} + {{svg "octicon-milestone" 16 "tw-mr-2"}} {{else if .PageIsPullList}} - {{svg "octicon-git-pull-request" 16 "gt-mr-3"}} + {{svg "octicon-git-pull-request" 16 "tw-mr-2"}} {{else}} - {{svg "octicon-issue-opened" 16 "gt-mr-3"}} + {{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{end}} {{ctx.Locale.PrettyNumber .OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} </a> <a class="{{if eq .State "closed"}}active {{end}}item" href="{{if eq .State "closed"}}{{.AllStatesLink}}{{else}}{{.ClosedLink}}{{end}}"> - {{svg "octicon-check" 16 "gt-mr-3"}} + {{svg "octicon-check" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} </a> </div> diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl index 361f16fd3b..769387b51c 100644 --- a/templates/repo/issue/search.tmpl +++ b/templates/repo/issue/search.tmpl @@ -9,10 +9,10 @@ <input type="hidden" name="assignee" value="{{$.AssigneeID}}"> <input type="hidden" name="poster" value="{{$.PosterID}}"> {{end}} - {{template "shared/searchinput" dict "Value" .Keyword}} + {{template "shared/search/input" dict "Value" .Keyword}} {{if .PageIsIssueList}} - <button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}" data-repo-link="{{.RepoLink}}">{{svg "octicon-hash"}}</button> + <button id="issue-list-quick-goto" class="ui small icon button tw-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}" data-repo-link="{{.RepoLink}}">{{svg "octicon-hash"}}</button> {{end}} - <button class="ui small icon button" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button> + {{template "shared/search/button"}} </div> </form> diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index ed444f6dce..c65b79dea7 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -20,18 +20,18 @@ </a> {{end}} <div class="content comment-container"> - <div class="ui top attached header comment-header gt-df gt-ac gt-sb" role="heading" aria-level="3"> - <div class="comment-header-left gt-df gt-ac"> + <div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3"> + <div class="comment-header-left tw-flex tw-items-center"> {{if .Issue.OriginalAuthor}} - <span class="text black gt-font-semibold"> + <span class="text black tw-font-semibold"> {{svg (MigrationIcon .Repository.GetOriginalURLHostname)}} {{.Issue.OriginalAuthor}} </span> <span class="text grey muted-links"> - {{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}} </span> <span class="text migrate"> - {{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" (.Repository.OriginalURL|Escape) (.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + {{if .Repository.OriginalURL}} ({{ctx.Locale.Tr "repo.migrated_from" .Repository.OriginalURL .Repository.GetOriginalURLHostname}}){{end}} </span> {{else}} <a class="inline-timeline-avatar" href="{{.Issue.Poster.HomeLink}}"> @@ -39,11 +39,11 @@ </a> <span class="text grey muted-links"> {{template "shared/user/authorlink" .Issue.Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.Issue.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr}} </span> {{end}} </div> - <div class="comment-header-right actions gt-df gt-ac"> + <div class="comment-header-right actions tw-flex tw-items-center"> {{template "repo/issue/view_content/show_role" dict "ShowRole" .Issue.ShowRole "IgnorePoster" true}} {{if not $.Repository.IsArchived}} {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}} @@ -54,15 +54,15 @@ <div class="ui attached segment comment-body" role="article"> <div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission $.IsIssuePoster}}data-can-edit="true"{{end}}> {{if .Issue.RenderedContent}} - {{.Issue.RenderedContent|Str2html}} + {{.Issue.RenderedContent}} {{else}} <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> {{end}} </div> - <div id="issue-{{.Issue.ID}}-raw" class="raw-content gt-hidden">{{.Issue.Content}}</div> - <div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div> + <div id="issue-{{.Issue.ID}}-raw" class="raw-content tw-hidden">{{.Issue.Content}}</div> + <div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}" data-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/attachments" data-view-attachment-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/view-attachments"></div> {{if .Issue.Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Issue.Attachments "Content" .Issue.RenderedContent}} + {{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}} {{end}} </div> {{$reactions := .Issue.Reactions.GroupByType}} @@ -114,7 +114,7 @@ </div> </div> {{else if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{if .Issue.IsPull}} {{ctx.Locale.Tr "repo.archive.pull.nocomment"}} {{else}} @@ -124,7 +124,7 @@ {{end}} {{else}} {{/* not .IsSigned */}} {{if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{if .Issue.IsPull}} {{ctx.Locale.Tr "repo.archive.pull.nocomment"}} {{else}} @@ -133,7 +133,7 @@ </div> {{else}} <div class="ui warning message"> - {{ctx.Locale.Tr "repo.issues.sign_in_require_desc" (.SignInLink|Escape) | Safe}} + {{ctx.Locale.Tr "repo.issues.sign_in_require_desc" .SignInLink}} </div> {{end}} {{end}}{{/* end if: .IsSigned */}} @@ -170,8 +170,9 @@ </template> {{template "repo/issue/view_content/reference_issue_dialog" .}} +{{template "shared/user/block_user_dialog" .}} -<div class="gt-hidden" id="no-content"> +<div class="tw-hidden" id="no-content"> <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> </div> @@ -181,7 +182,7 @@ {{ctx.Locale.Tr "repo.branch.delete" .HeadTarget}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "repo.branch.delete_desc" | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl index 1fb6f2f2c2..2155f78656 100644 --- a/templates/repo/issue/view_content/attachments.tmpl +++ b/templates/repo/issue/view_content/attachments.tmpl @@ -4,11 +4,11 @@ {{end}} {{$hasThumbnails := false}} {{- range .Attachments -}} - <div class="gt-df"> - <div class="gt-f1 gt-p-3"> + <div class="tw-flex"> + <div class="tw-flex-1 tw-p-2"> <a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}"> {{if FilenameIsImage .Name}} - {{if not (StringUtils.Contains $.Content .UUID)}} + {{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}} {{$hasThumbnails = true}} {{end}} {{svg "octicon-file"}} @@ -18,7 +18,7 @@ <span><strong>{{.Name}}</strong></span> </a> </div> - <div class="gt-p-3 gt-df gt-ac"> + <div class="tw-p-2 tw-flex tw-items-center"> <span class="ui text grey">{{.Size | FileSize}}</span> </div> </div> @@ -29,7 +29,7 @@ <div class="ui small thumbnails"> {{- range .Attachments -}} {{if FilenameIsImage .Name}} - {{if not (StringUtils.Contains $.Content .UUID)}} + {{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}} <a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}"> <img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}"> </a> diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index ade0ea34cf..f65dc6ee90 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -25,18 +25,18 @@ </a> {{end}} <div class="content comment-container"> - <div class="ui top attached header comment-header gt-df gt-ac gt-sb" role="heading" aria-level="3"> - <div class="comment-header-left gt-df gt-ac"> + <div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between" role="heading" aria-level="3"> + <div class="comment-header-left tw-flex tw-items-center"> {{if .OriginalAuthor}} - <span class="text black gt-font-semibold gt-mr-2"> + <span class="text black tw-font-semibold tw-mr-1"> {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} {{.OriginalAuthor}} </span> <span class="text grey muted-links"> - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} {{if $.Repository.OriginalURL}} + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} {{if $.Repository.OriginalURL}} </span> <span class="text migrate"> - ({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}} + ({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}} </span> {{else}} {{if gt .Poster.ID 0}} @@ -46,11 +46,11 @@ {{end}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdStr}} </span> {{end}} </div> - <div class="comment-header-right actions gt-df gt-ac"> + <div class="comment-header-right actions tw-flex tw-items-center"> {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} {{if not $.Repository.IsArchived}} {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} @@ -61,15 +61,15 @@ <div class="ui attached segment comment-body" role="article"> <div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}> {{if .RenderedContent}} - {{.RenderedContent|Str2html}} + {{.RenderedContent}} {{else}} <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> {{end}} </div> - <div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div> - <div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div> + <div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div> + <div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div> {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} {{end}} </div> {{$reactions := .Reactions.GroupByType}} @@ -80,48 +80,54 @@ </div> {{else if eq .Type 1}} <div class="timeline-item event" id="{{.HashTag}}"> - <span class="badge gt-bg-green gt-text-white">{{svg "octicon-dot-fill"}}</span> - {{template "shared/user/avatarlink" dict "user" .Poster}} + <span class="badge tw-bg-green tw-text-white">{{svg "octicon-dot-fill"}}</span> + {{if not .OriginalAuthor}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} <span class="text grey muted-links"> - {{template "shared/user/authorlink" .Poster}} + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.pulls.reopened_at" .EventTag $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.reopened_at" .EventTag $createdStr}} {{end}} </span> </div> {{else if eq .Type 2}} <div class="timeline-item event" id="{{.HashTag}}"> - <span class="badge gt-bg-red gt-text-white">{{svg "octicon-circle-slash"}}</span> - {{template "shared/user/avatarlink" dict "user" .Poster}} + <span class="badge tw-bg-red tw-text-white">{{svg "octicon-circle-slash"}}</span> + {{if not .OriginalAuthor}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} <span class="text grey muted-links"> - {{template "shared/user/authorlink" .Poster}} + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{if .Issue.IsPull}} - {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.pulls.closed_at" .EventTag $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.closed_at" .EventTag $createdStr}} {{end}} </span> </div> {{else if eq .Type 28}} <div class="timeline-item event" id="{{.HashTag}}"> - <span class="badge gt-bg-purple gt-text-white">{{svg "octicon-git-merge"}}</span> - {{template "shared/user/avatarlink" dict "user" .Poster}} + <span class="badge tw-bg-purple tw-text-white">{{svg "octicon-git-merge"}}</span> + {{if not .OriginalAuthor}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} <span class="text grey muted-links"> - {{template "shared/user/authorlink" .Poster}} + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} {{$link := printf "%s/commit/%s" $.Repository.Link ($.Issue.PullRequest.MergedCommitID|PathEscape)}} {{if eq $.Issue.PullRequest.Status 3}} - {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.comment_manually_pull_merged_at" (HTMLFormat `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "<b>%[1]s</b>" $.BaseTarget) $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (printf `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` ($link|Escape) (ShortSha $.Issue.PullRequest.MergedCommitID)) (printf "<b>%[1]s</b>" ($.BaseTarget|Escape)) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.comment_pull_merged_at" (HTMLFormat `<a class="ui sha" href="%[1]s"><b>%[2]s</b></a>` $link (ShortSha $.Issue.PullRequest.MergedCommitID)) (HTMLFormat "<b>%[1]s</b>" $.BaseTarget) $createdStr}} {{end}} </span> </div> {{else if eq .Type 3 5 6}} {{$refFrom:= ""}} {{if ne .RefRepoID .Issue.RepoID}} - {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" (.RefRepo.FullName|Escape)}} + {{$refFrom = ctx.Locale.Tr "repo.issues.ref_from" .RefRepo.FullName}} {{end}} {{$refTr := "repo.issues.ref_issue_from"}} {{if .Issue.IsPull}} @@ -138,11 +144,11 @@ {{if eq .RefAction 3}}<del>{{end}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr $refTr (.EventTag|Escape) $createdStr ((.RefCommentLink ctx)|Escape) $refFrom | Safe}} + {{ctx.Locale.Tr $refTr .EventTag $createdStr (.RefCommentLink ctx) $refFrom}} </span> {{if eq .RefAction 3}}</del>{{end}} - <div class="detail"> + <div class="detail flex-text-block"> <span class="text grey muted-links"><a href="{{.RefIssueLink ctx}}"><b>{{.RefIssueTitle ctx}}</b> {{.RefIssueIdent ctx}}</a></span> </div> </div> @@ -152,11 +158,11 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.commit_ref_at" .EventTag $createdStr}} </span> - <div class="detail"> + <div class="detail flex-text-block"> {{svg "octicon-git-commit"}} - <span class="text grey muted-links">{{.Content | Str2html}}</span> + <span class="text grey muted-links">{{.Content | SanitizeHTML}}</span> </div> </div> {{else if eq .Type 7}} @@ -167,11 +173,11 @@ <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} {{if and .AddedLabels (not .RemovedLabels)}} - {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) $createdStr | Safe}} + {{ctx.Locale.TrN (len .AddedLabels) "repo.issues.add_label" "repo.issues.add_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) $createdStr}} {{else if and (not .AddedLabels) .RemovedLabels}} - {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{ctx.Locale.TrN (len .RemovedLabels) "repo.issues.remove_label" "repo.issues.remove_labels" (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context .AddedLabels $.RepoLink) (RenderLabels $.Context .RemovedLabels $.RepoLink) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_remove_labels" (RenderLabels $.Context ctx.Locale .AddedLabels $.RepoLink) (RenderLabels $.Context ctx.Locale .RemovedLabels $.RepoLink) $createdStr}} {{end}} </span> </div> @@ -182,7 +188,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" (.OldMilestone.Name|Escape) (.Milestone.Name|Escape) $createdStr | Safe}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" (.OldMilestone.Name|Escape) $createdStr | Safe}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" (.Milestone.Name|Escape) $createdStr | Safe}}{{end}} + {{if gt .OldMilestoneID 0}}{{if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.change_milestone_at" .OldMilestone.Name .Milestone.Name $createdStr}}{{else}}{{ctx.Locale.Tr "repo.issues.remove_milestone_at" .OldMilestone.Name $createdStr}}{{end}}{{else if gt .MilestoneID 0}}{{ctx.Locale.Tr "repo.issues.add_milestone_at" .Milestone.Name $createdStr}}{{end}} </span> </div> {{else if and (eq .Type 9) (gt .AssigneeID 0)}} @@ -193,9 +199,9 @@ <span class="text grey muted-links"> {{template "shared/user/authorlink" .Assignee}} {{if eq .Poster.ID .Assignee.ID}} - {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_self_assignment" $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.remove_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_assignee_at" .Poster.GetDisplayName $createdStr}} {{end}} </span> {{else}} @@ -203,9 +209,9 @@ <span class="text grey muted-links"> {{template "shared/user/authorlink" .Assignee}} {{if eq .Poster.ID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.self_assign_at" $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_assignee_at" (.Poster.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_assignee_at" .Poster.GetDisplayName $createdStr}} {{end}} </span> {{end}} @@ -216,7 +222,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.change_title_at" (.OldTitle|RenderEmoji $.Context) (.NewTitle|RenderEmoji $.Context) $createdStr}} </span> </div> {{else if eq .Type 11}} @@ -225,7 +231,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.delete_branch_at" .OldRef $createdStr}} </span> </div> {{else if eq .Type 12}} @@ -234,7 +240,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.start_tracking_history" $createdStr}} </span> </div> {{else if eq .Type 13}} @@ -243,10 +249,10 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}} </span> {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} - <div class="detail"> + <div class="detail flex-text-block"> {{svg "octicon-clock"}} {{if .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} @@ -262,10 +268,10 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}} </span> {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} - <div class="detail"> + <div class="detail flex-text-block"> {{svg "octicon-clock"}} {{if .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} @@ -281,7 +287,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.cancel_tracking_history" $createdStr}} </span> </div> {{else if eq .Type 16}} @@ -290,7 +296,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr}} </span> </div> {{else if eq .Type 17}} @@ -303,7 +309,7 @@ {{if eq (len $parsedDeadline) 2}} {{$from := DateTime "long" (index $parsedDeadline 1)}} {{$to := DateTime "long" (index $parsedDeadline 0)}} - {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr}} {{end}} </span> </div> @@ -313,7 +319,7 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr}} </span> </div> {{else if eq .Type 19}} @@ -322,10 +328,10 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.dependency.added_dependency" $createdStr}} </span> {{if .DependentIssue}} - <div class="detail"> + <div class="detail flex-text-block"> {{svg "octicon-plus"}} <span class="text grey muted-links"> <a href="{{.DependentIssue.Link}}"> @@ -345,11 +351,11 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.dependency.removed_dependency" $createdStr}} </span> {{if .DependentIssue}} - <div class="detail"> - <span class="text grey muted-links">{{svg "octicon-trash"}}</span> + <div class="detail flex-text-block"> + {{svg "octicon-trash"}} <span class="text grey muted-links"> <a href="{{.DependentIssue.Link}}"> {{if eq .DependentIssue.RepoID .Issue.RepoID}} @@ -365,38 +371,30 @@ {{else if eq .Type 22}} <div class="timeline-item-group" id="{{.HashTag}}"> <div class="timeline-item event"> - {{if .OriginalAuthor}} - {{else}} - {{/* Some timeline avatars need a offset to correctly allign with their speech - bubble. The condition depends on review type and for positive reviews whether - there is a comment element or not */}} - <a class="timeline-avatar{{if or (and (eq .Review.Type 1) (or .Content .Attachments)) (and (eq .Review.Type 2) (or .Content .Attachments)) (eq .Review.Type 3)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> + {{$reviewType := -1}} + {{if .Review}}{{$reviewType = .Review.Type}}{{end}} + {{if not .OriginalAuthor}} + {{/* Some timeline avatars need a offset to correctly align with their speech bubble. + The condition depends on whether the comment has contents/attachments or reviews */}} + <a class="timeline-avatar{{if or .Content .Attachments (and .Review .Review.CodeComments)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> {{ctx.AvatarUtils.Avatar .Poster 40}} </a> {{end}} - <span class="badge{{if eq .Review.Type 1}} gt-bg-green gt-text-white{{else if eq .Review.Type 3}} gt-bg-red gt-text-white{{end}}">{{svg (printf "octicon-%s" .Review.Type.Icon)}}</span> + <span class="badge{{if eq $reviewType 1}} tw-bg-green tw-text-white{{else if eq $reviewType 3}} tw-bg-red tw-text-white{{end}}"> + {{if .Review}}{{svg (printf "octicon-%s" .Review.Type.Icon)}}{{end}} + </span> <span class="text grey muted-links"> - {{if .OriginalAuthor}} - <span class="text black"> - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - </span> - <span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span> - <span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span> + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} + {{if eq $reviewType 1}} + {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr}} + {{else if eq $reviewType 2}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}} + {{else if eq $reviewType 3}} + {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr}} {{else}} - {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr}} {{end}} - - {{if eq .Review.Type 1}} - {{ctx.Locale.Tr "repo.issues.review.approve" $createdStr | Safe}} - {{else if eq .Review.Type 2}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{else if eq .Review.Type 3}} - {{ctx.Locale.Tr "repo.issues.review.reject" $createdStr | Safe}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.comment" $createdStr | Safe}} - {{end}} - {{if .Review.Dismissed}} + {{if and .Review .Review.Dismissed}} <div class="ui small label">{{ctx.Locale.Tr "repo.issues.review.dismissed_label"}}</div> {{end}} </span> @@ -404,8 +402,8 @@ {{if or .Content .Attachments}} <div class="timeline-item comment"> <div class="content comment-container"> - <div class="ui top attached header comment-header gt-df gt-ac gt-sb"> - <div class="comment-header-left gt-df gt-ac"> + <div class="ui top attached header comment-header tw-flex tw-items-center tw-justify-between"> + <div class="comment-header-left tw-flex tw-items-center"> {{if gt .Poster.ID 0}} <a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}"> {{ctx.AvatarUtils.Avatar .Poster 24}} @@ -413,20 +411,20 @@ {{end}} <span class="text grey muted-links"> {{if .OriginalAuthor}} - <span class="text black gt-font-semibold"> + <span class="text black tw-font-semibold"> {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} {{.OriginalAuthor}} </span> <span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span> - <span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span> + <span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}}){{end}}</span> {{else}} {{template "shared/user/authorlink" .Poster}} {{end}} - {{ctx.Locale.Tr "repo.issues.review.left_comment" | Safe}} + {{ctx.Locale.Tr "repo.issues.review.left_comment"}} </span> </div> - <div class="comment-header-right actions gt-df gt-ac"> + <div class="comment-header-right actions tw-flex tw-items-center"> {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} {{if not $.Repository.IsArchived}} {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} @@ -437,15 +435,15 @@ <div class="ui attached segment comment-body"> <div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}> {{if .RenderedContent}} - {{.RenderedContent|Str2html}} + {{.RenderedContent}} {{else}} <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> {{end}} </div> - <div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div> - <div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div> + <div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div> + <div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div> {{if .Attachments}} - {{template "repo/issue/view_content/attachments" dict "ctxData" $ "Attachments" .Attachments "Content" .RenderedContent}} + {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} {{end}} </div> {{$reactions := .Reactions.GroupByType}} @@ -456,7 +454,7 @@ </div> {{end}} - {{if .Review.CodeComments}} + {{if and .Review .Review.CodeComments}} <div class="timeline-item event"> {{range $filename, $lines := .Review.CodeComments}} {{range $line, $comms := $lines}} @@ -473,12 +471,12 @@ {{if .Content}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.lock_with_reason" .Content $createdStr}} </span> {{else}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.lock_no_reason" $createdStr}} </span> {{end}} </div> @@ -488,16 +486,18 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.unlock_comment" $createdStr}} </span> </div> {{else if eq .Type 25}} <div class="timeline-item event"> <span class="badge">{{svg "octicon-git-branch"}}</span> - {{template "shared/user/avatarlink" dict "user" .Poster}} + {{if not .OriginalAuthor}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + {{end}} <span class="text grey muted-links"> - <a{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>{{.Poster.Name}}</a> - {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} + {{ctx.Locale.Tr "repo.pulls.change_target_branch_at" .OldRef .NewRef $createdStr}} </span> </div> {{else if eq .Type 26}} @@ -507,9 +507,9 @@ <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.del_time_history" $createdStr}} </span> - <div class="detail"> + <div class="detail flex-text-block"> {{svg "octicon-clock"}} {{if .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} @@ -528,12 +528,12 @@ {{if (gt .AssigneeID 0)}} {{if .RemovedAssignee}} {{if eq .PosterID .AssigneeID}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request_self" $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" .Assignee.GetDisplayName $createdStr}} {{end}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" (.Assignee.GetDisplayName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" .Assignee.GetDisplayName $createdStr}} {{end}} {{else}} <!-- If the assigned team is deleted, just displaying "Ghost Team" in the comment --> @@ -542,9 +542,9 @@ {{$teamName = .AssigneeTeam.Name}} {{end}} {{if .RemovedAssignee}} - {{ctx.Locale.Tr "repo.issues.review.remove_review_request" ($teamName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.remove_review_request" $teamName $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.add_review_request" ($teamName|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.add_review_request" $teamName $createdStr}} {{end}} {{end}} </span> @@ -559,13 +559,13 @@ <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} {{if .IsForcePush}} - {{ctx.Locale.Tr "repo.issues.force_push_codes" ($.Issue.PullRequest.HeadBranch|Escape) (ShortSha .OldCommit) (($.Issue.Repo.CommitLink .OldCommit)|Escape) (ShortSha .NewCommit) (($.Issue.Repo.CommitLink .NewCommit)|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.force_push_codes" $.Issue.PullRequest.HeadBranch (ShortSha .OldCommit) ($.Issue.Repo.CommitLink .OldCommit) (ShortSha .NewCommit) ($.Issue.Repo.CommitLink .NewCommit) $createdStr}} {{else}} - {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr | Safe}} + {{ctx.Locale.TrN (len .Commits) "repo.issues.push_commit_1" "repo.issues.push_commits_n" (len .Commits) $createdStr}} {{end}} </span> {{if and .IsForcePush $.Issue.PullRequest.BaseRepo.Name}} - <span class="gt-float-right comparebox"> + <span class="tw-float-right comparebox"> <a href="{{$.Issue.PullRequest.BaseRepo.Link}}/compare/{{PathEscape .OldCommit}}..{{PathEscape .NewCommit}}" rel="nofollow" class="ui compare label">{{ctx.Locale.Tr "repo.issues.force_push_compare"}}</a> </span> {{end}} @@ -583,19 +583,19 @@ {{$oldProjectDisplayHtml := "Unknown Project"}} {{if .OldProject}} {{$trKey := printf "projects.type-%d.display_name" .OldProject.Type}} - {{$oldProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.OldProject.Title | Escape)}} + {{$oldProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .OldProject.Title}} {{end}} {{$newProjectDisplayHtml := "Unknown Project"}} {{if .Project}} {{$trKey := printf "projects.type-%d.display_name" .Project.Type}} - {{$newProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} + {{$newProjectDisplayHtml = HTMLFormat `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey) .Project.Title}} {{end}} {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} - {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr}} {{else if gt .OldProjectID 0}} - {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr}} {{else if gt .ProjectID 0}} - {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr}} {{end}} </span> </div> @@ -610,18 +610,20 @@ <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} {{$reviewerName := ""}} - {{if eq .Review.OriginalAuthor ""}} - {{$reviewerName = .Review.Reviewer.Name}} - {{else}} - {{$reviewerName = .Review.OriginalAuthor}} + {{if .Review}} + {{if eq .Review.OriginalAuthor ""}} + {{$reviewerName = .Review.Reviewer.Name}} + {{else}} + {{$reviewerName = .Review.OriginalAuthor}} + {{end}} {{end}} - {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.review.dismissed" $reviewerName $createdStr}} </span> </div> {{if .Content}} <div class="timeline-item comment"> <div class="content"> - <div class="ui top attached header comment-header-left gt-df gt-ac arrow-top"> + <div class="ui top attached header comment-header-left tw-flex tw-items-center arrow-top"> {{if gt .Poster.ID 0}} <a class="inline-timeline-avatar" href="{{.Poster.HomeLink}}"> {{ctx.AvatarUtils.Avatar .Poster 24}} @@ -634,7 +636,7 @@ <div class="ui attached segment"> <div class="render-content markup"> {{if .RenderedContent}} - {{.RenderedContent|Str2html}} + {{.RenderedContent}} {{else}} <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> {{end}} @@ -651,11 +653,11 @@ <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} {{if and .OldRef .NewRef}} - {{ctx.Locale.Tr "repo.issues.change_ref_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.change_ref_at" .OldRef .NewRef $createdStr}} {{else if .OldRef}} - {{ctx.Locale.Tr "repo.issues.remove_ref_at" (.OldRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.remove_ref_at" .OldRef $createdStr}} {{else}} - {{ctx.Locale.Tr "repo.issues.add_ref_at" (.NewRef|Escape) $createdStr | Safe}} + {{ctx.Locale.Tr "repo.issues.add_ref_at" .NewRef $createdStr}} {{end}} </span> </div> @@ -663,9 +665,9 @@ <div class="timeline-item event" id="{{.HashTag}}"> <span class="badge">{{svg "octicon-git-merge" 16}}</span> <span class="text grey muted-links"> - {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} + {{template "repo/issue/view_content/comments_authorlink" dict "ctxData" $ "comment" .}} + {{if eq .Type 34}}{{ctx.Locale.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr}} + {{else}}{{ctx.Locale.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr}}{{end}} </span> </div> {{else if or (eq .Type 36) (eq .Type 37)}} @@ -674,8 +676,8 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> {{template "shared/user/authorlink" .Poster}} - {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr | Safe}} - {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr | Safe}}{{end}} + {{if eq .Type 36}}{{ctx.Locale.Tr "repo.issues.pin_comment" $createdStr}} + {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}} </span> </div> {{end}} diff --git a/templates/repo/issue/view_content/comments_authorlink.tmpl b/templates/repo/issue/view_content/comments_authorlink.tmpl new file mode 100644 index 0000000000..f652a0bec3 --- /dev/null +++ b/templates/repo/issue/view_content/comments_authorlink.tmpl @@ -0,0 +1,11 @@ +{{if .comment.OriginalAuthor}} + <span class="text black"> + {{svg (MigrationIcon .ctxData.Repository.GetOriginalURLHostname)}} + {{.comment.OriginalAuthor}} + </span> + {{if .ctxData.Repository.OriginalURL}} + <span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" .ctxData.Repository.OriginalURL .ctxData.Repository.GetOriginalURLHostname}})</span> + {{end}} +{{else}} + {{template "shared/user/authorlink" .comment.Poster}} +{{end}} diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl index 7c01bb4228..2377e7c4f0 100644 --- a/templates/repo/issue/view_content/comments_delete_time.tmpl +++ b/templates/repo/issue/view_content/comments_delete_time.tmpl @@ -1,7 +1,7 @@ -{{if .comment.Time}} {{/* compatibility with time comments made before v1.14 */}} +{{if and .comment.Time (.ctxData.Repository.IsTimetrackerEnabled ctx)}} {{/* compatibility with time comments made before v1.14 */}} {{if (not .comment.Time.Deleted)}} {{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}} - <span class="gt-float-right"> + <span class="tw-float-right"> <div class="ui mini modal issue-delete-time-modal" data-id="{{.comment.Time.ID}}"> <form method="post" class="delete-time-form" action="{{.ctxData.RepoLink}}/issues/{{.ctxData.Issue.Index}}/times/{{.comment.TimeID}}/delete"> {{.ctxData.CsrfTokenHtml}} diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl index 4afd73c371..17556d4e48 100644 --- a/templates/repo/issue/view_content/context_menu.tmpl +++ b/templates/repo/issue/view_content/context_menu.tmpl @@ -10,16 +10,33 @@ {{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}} {{end}} <div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.copy_link"}}</div> - {{if and .ctxData.IsSigned (not .ctxData.Repository.IsArchived)}} - <div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div> - {{if not .ctxData.UnitIssuesGlobalDisabled}} - <div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div> + {{if .ctxData.IsSigned}} + {{$needDivider := false}} + {{if not .ctxData.Repository.IsArchived}} + {{$needDivider = true}} + <div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{ctx.Locale.Tr "repo.issues.context.quote_reply"}}</div> + {{if not .ctxData.UnitIssuesGlobalDisabled}} + <div class="item context js-aria-clickable reference-issue" data-target="{{.item.HashTag}}-raw" data-modal="#reference-issue-modal" data-poster="{{.item.Poster.GetDisplayName}}" data-poster-username="{{.item.Poster.Name}}" data-reference="{{$referenceUrl}}">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</div> + {{end}} + {{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}} + <div class="divider"></div> + <div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div> + {{if .delete}} + <div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div> + {{end}} + {{end}} {{end}} - {{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}} - <div class="divider"></div> - <div class="item context js-aria-clickable edit-content">{{ctx.Locale.Tr "repo.issues.context.edit"}}</div> - {{if .delete}} - <div class="item context js-aria-clickable delete-comment" data-comment-id={{.item.HashTag}} data-url="{{.ctxData.RepoLink}}/comments/{{.item.ID}}/delete" data-locale="{{ctx.Locale.Tr "repo.issues.delete_comment_confirm"}}">{{ctx.Locale.Tr "repo.issues.context.delete"}}</div> + {{$canUserBlock := call .ctxData.CanBlockUser .ctxData.SignedUser .item.Poster}} + {{$canOrgBlock := and .ctxData.Repository.Owner.IsOrganization (call .ctxData.CanBlockUser .ctxData.Repository.Owner .item.Poster)}} + {{if or $canOrgBlock $canUserBlock}} + {{if $needDivider}} + <div class="divider"></div> + {{end}} + {{if $canUserBlock}} + <div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</div> + {{end}} + {{if $canOrgBlock}} + <div class="item context js-aria-clickable show-modal" data-modal="#block-user-modal" data-modal-modal-blockee="{{.item.Poster.Name}}" data-modal-modal-blockee-name="{{.item.Poster.GetDisplayName}}" data-modal-modal-form.action="{{.ctxData.Repository.Owner.OrganisationLink}}/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.org"}}</div> {{end}} {{end}} {{end}} diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl index c9e5ee6275..79e7cb498b 100644 --- a/templates/repo/issue/view_content/conversation.tmpl +++ b/templates/repo/issue/view_content/conversation.tmpl @@ -1,133 +1,143 @@ -{{$invalid := (index .comments 0).Invalidated}} -{{$resolved := (index .comments 0).IsResolved}} -{{$resolveDoer := (index .comments 0).ResolveDoer}} -{{$isNotPending := (not (eq (index .comments 0).Review.Type 0))}} -<div class="ui segments conversation-holder"> - <div class="ui segment collapsible-comment-box gt-py-3 gt-df gt-ac gt-sb"> - <div class="gt-df gt-ac"> - <a href="{{(index .comments 0).CodeCommentLink ctx}}" class="file-comment gt-ml-3 gt-word-break">{{(index .comments 0).TreePath}}</a> - {{if $invalid}} - <span class="ui label basic small gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}"> - {{ctx.Locale.Tr "repo.issues.review.outdated"}} - </span> - {{end}} - </div> - <div> - {{if or $invalid $resolved}} - <button id="show-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if not $resolved}}gt-hidden {{end}}ui compact labeled button show-outdated gt-df gt-ac"> - {{svg "octicon-unfold" 16 "gt-mr-3"}} - {{if $resolved}} - {{ctx.Locale.Tr "repo.issues.review.show_resolved"}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.show_outdated"}} - {{end}} - </button> - <button id="hide-outdated-{{(index .comments 0).ID}}" data-comment="{{(index .comments 0).ID}}" class="{{if $resolved}}gt-hidden {{end}}ui compact labeled button hide-outdated gt-df gt-ac"> - {{svg "octicon-fold" 16 "gt-mr-3"}} - {{if $resolved}} - {{ctx.Locale.Tr "repo.issues.review.hide_resolved"}} - {{else}} - {{ctx.Locale.Tr "repo.issues.review.hide_outdated"}} - {{end}} - </button> - {{end}} - </div> - </div> - {{$diff := (CommentMustAsDiff ctx (index .comments 0))}} - {{if $diff}} - {{$file := (index $diff.Files 0)}} - <div id="code-preview-{{(index .comments 0).ID}}" class="ui table segment{{if $resolved}} gt-hidden{{end}}"> - <div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}"> - <div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped"> - <table> - <tbody> - {{template "repo/diff/section_unified" dict "file" $file "root" $}} - </tbody> - </table> - </div> - </div> - </div> - {{end}} - <div id="code-comments-{{(index .comments 0).ID}}" class="comment-code-cloud ui segment{{if $resolved}} gt-hidden{{end}}"> - <div class="ui comments gt-mb-0"> - {{range .comments}} - {{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} - <div class="comment code-comment gt-pb-4" id="{{.HashTag}}"> - <div class="content"> - <div class="header comment-header"> - <div class="comment-header-left gt-df gt-ac"> - {{if not .OriginalAuthor}} - <a class="avatar"> - {{ctx.AvatarUtils.Avatar .Poster 20}} - </a> - {{end}} - <span class="text grey muted-links"> - {{if .OriginalAuthor}} - <span class="text black gt-font-semibold"> - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} - {{.OriginalAuthor}} - </span> - <span class="text grey muted-links"> {{if $.Repository.OriginalURL}}</span> - <span class="text migrate">({{ctx.Locale.Tr "repo.migrated_from" ($.Repository.OriginalURL|Escape) ($.Repository.GetOriginalURLHostname|Escape) | Safe}}){{end}}</span> - {{else}} - {{template "shared/user/authorlink" .Poster}} - {{end}} - {{ctx.Locale.Tr "repo.issues.commented_at" (.HashTag|Escape) $createdSubStr | Safe}} - </span> - </div> - <div class="comment-header-right actions gt-df gt-ac"> - {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} - {{if not $.Repository.IsArchived}} - {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} - {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} - {{end}} - </div> - </div> - <div class="text comment-content"> - <div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}> - {{if .RenderedContent}} - {{.RenderedContent|Str2html}} - {{else}} - <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> - {{end}} - </div> - <div id="issuecomment-{{.ID}}-raw" class="raw-content gt-hidden">{{.Content}}</div> - <div class="edit-content-zone gt-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div> - </div> - {{$reactions := .Reactions.GroupByType}} - {{if $reactions}} - {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} - {{end}} - </div> - </div> - {{end}} - </div> - <div class="code-comment-buttons gt-df gt-ac gt-fw gt-mt-3 gt-mb-2 gt-mx-3"> - <div class="gt-f1"> - {{if $resolved}} - <div class="ui grey text"> - {{svg "octicon-check" 16 "gt-mr-2"}} - <b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}} - </div> +{{if len .comments}} + {{$comment := index .comments 0}} + {{$invalid := $comment.Invalidated}} + {{$resolved := $comment.IsResolved}} + {{$resolveDoer := $comment.ResolveDoer}} + {{$hasReview := and $comment.Review}} + {{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}} + <div class="ui segments conversation-holder"> + <div class="ui segment collapsible-comment-box tw-py-2 tw-flex tw-items-center tw-justify-between"> + <div class="tw-flex tw-items-center"> + <a href="{{$comment.CodeCommentLink ctx}}" class="file-comment tw-ml-2 gt-word-break">{{$comment.TreePath}}</a> + {{if $invalid}} + <span class="ui label basic small tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}"> + {{ctx.Locale.Tr "repo.issues.review.outdated"}} + </span> {{end}} </div> - <div class="code-comment-buttons-buttons"> - {{if and $.CanMarkConversation $isNotPending}} - <button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{(index .comments 0).ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation"> + <div> + {{if or $invalid $resolved}} + <button id="show-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if not $resolved}}tw-hidden {{end}}ui compact labeled button show-outdated tw-flex tw-items-center"> + {{svg "octicon-unfold" 16 "tw-mr-2"}} {{if $resolved}} - {{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}} + {{ctx.Locale.Tr "repo.issues.review.show_resolved"}} {{else}} - {{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}} + {{ctx.Locale.Tr "repo.issues.review.show_outdated"}} {{end}} </button> - {{end}} - {{if and $.SignedUserID (not $.Repository.IsArchived)}} - <button class="comment-form-reply ui primary tiny labeled icon button gt-ml-2 gt-mr-0"> - {{svg "octicon-reply" 16 "reply icon gt-mr-2"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}} + <button id="hide-outdated-{{$comment.ID}}" data-comment="{{$comment.ID}}" class="{{if $resolved}}tw-hidden {{end}}ui compact labeled button hide-outdated tw-flex tw-items-center"> + {{svg "octicon-fold" 16 "tw-mr-2"}} + {{if $resolved}} + {{ctx.Locale.Tr "repo.issues.review.hide_resolved"}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.hide_outdated"}} + {{end}} </button> {{end}} </div> </div> - {{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" (index .comments 0).ReviewID "root" $ "comment" (index .comments 0)}} + {{$diff := (CommentMustAsDiff ctx $comment)}} + {{if $diff}} + {{$file := (index $diff.Files 0)}} + <div id="code-preview-{{$comment.ID}}" class="ui table segment{{if $resolved}} tw-hidden{{end}}"> + <div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}"> + <div class="file-body file-code code-view code-diff code-diff-unified unicode-escaped"> + <table> + <tbody> + {{template "repo/diff/section_unified" dict "file" $file "root" $}} + </tbody> + </table> + </div> + </div> + </div> + {{end}} + <div id="code-comments-{{$comment.ID}}" class="comment-code-cloud ui segment{{if $resolved}} tw-hidden{{end}}"> + <div class="ui comments tw-mb-0"> + {{range .comments}} + {{$createdSubStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} + <div class="comment code-comment tw-pb-4" id="{{.HashTag}}"> + <div class="content"> + <div class="header comment-header"> + <div class="comment-header-left tw-flex tw-items-center"> + {{if not .OriginalAuthor}} + <a class="avatar"> + {{ctx.AvatarUtils.Avatar .Poster 20}} + </a> + {{end}} + <span class="text grey muted-links"> + {{if .OriginalAuthor}} + <span class="text black"> + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname)}} + {{.OriginalAuthor}} + </span> + {{if $.Repository.OriginalURL}} + <span class="migrate">({{ctx.Locale.Tr "repo.migrated_from" $.Repository.OriginalURL $.Repository.GetOriginalURLHostname}})</span> + {{end}} + {{else}} + {{template "shared/user/authorlink" .Poster}} + {{end}} + {{ctx.Locale.Tr "repo.issues.commented_at" .HashTag $createdSubStr}} + </span> + </div> + <div class="comment-header-right actions tw-flex tw-items-center"> + {{template "repo/issue/view_content/show_role" dict "ShowRole" .ShowRole}} + {{if not $.Repository.IsArchived}} + {{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}} + {{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}} + {{end}} + </div> + </div> + <div class="text comment-content"> + <div class="render-content markup" {{if or $.Permission.IsAdmin $.HasIssuesOrPullsWritePermission (and $.IsSigned (eq $.SignedUserID .PosterID))}}data-can-edit="true"{{end}}> + {{if .RenderedContent}} + {{.RenderedContent}} + {{else}} + <span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span> + {{end}} + </div> + <div id="issuecomment-{{.ID}}-raw" class="raw-content tw-hidden">{{.Content}}</div> + <div class="edit-content-zone tw-hidden" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}" data-attachment-url="{{$.RepoLink}}/comments/{{.ID}}/attachments"></div> + {{if .Attachments}} + {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} + {{end}} + </div> + {{$reactions := .Reactions.GroupByType}} + {{if $reactions}} + {{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}} + {{end}} + </div> + </div> + {{end}} + </div> + <div class="code-comment-buttons tw-flex tw-items-center tw-flex-wrap tw-mt-2 tw-mb-1 tw-mx-2"> + <div class="tw-flex-1"> + {{if $resolved}} + <div class="ui grey text"> + {{svg "octicon-check" 16 "tw-mr-1"}} + <b>{{$resolveDoer.Name}}</b> {{ctx.Locale.Tr "repo.issues.review.resolved_by"}} + </div> + {{end}} + </div> + <div class="code-comment-buttons-buttons"> + {{if and $.CanMarkConversation $hasReview (not $isReviewPending)}} + <button class="ui tiny basic button resolve-conversation" data-origin="timeline" data-action="{{if not $resolved}}Resolve{{else}}UnResolve{{end}}" data-comment-id="{{$comment.ID}}" data-update-url="{{$.RepoLink}}/issues/resolve_conversation"> + {{if $resolved}} + {{ctx.Locale.Tr "repo.issues.review.un_resolve_conversation"}} + {{else}} + {{ctx.Locale.Tr "repo.issues.review.resolve_conversation"}} + {{end}} + </button> + {{end}} + {{if and $.SignedUserID (not $.Repository.IsArchived)}} + <button class="comment-form-reply ui primary tiny labeled icon button tw-ml-1 tw-mr-0"> + {{svg "octicon-reply" 16 "reply icon tw-mr-1"}}{{ctx.Locale.Tr "repo.diff.comment.reply"}} + </button> + {{end}} + </div> + </div> + {{template "repo/diff/comment_form_datahandler" dict "hidden" true "reply" $comment.ReviewID "root" $ "comment" $comment}} + </div> </div> -</div> +{{else}} + {{template "repo/diff/conversation_outdated"}} +{{end}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 2b5776ea03..25e9bbcfc2 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -24,6 +24,7 @@ {{template "repo/pulls/status" (dict "CommitStatus" .LatestCommitStatus "CommitStatuses" .LatestCommitStatuses + "MissingRequiredChecks" .MissingRequiredChecks "ShowHideChecks" true "is_context_required" .is_context_required )}} @@ -32,13 +33,13 @@ <div class="ui attached merge-section segment {{if not $.LatestCommitStatus}}no-header{{end}} flex-items-block"> {{if .Issue.PullRequest.HasMerged}} {{if .IsPullBranchDeletable}} - <div class="item item-section text gt-f1"> + <div class="item item-section text tw-flex-1"> <div class="item-section-left"> - <h3 class="gt-mb-3"> + <h3 class="tw-mb-2"> {{ctx.Locale.Tr "repo.pulls.merged_success"}} </h3> <div class="merge-section-info"> - {{ctx.Locale.Tr "repo.pulls.merged_info_text" (printf "<code>%s</code>" (.HeadTarget | Escape)) | Str2html}} + {{ctx.Locale.Tr "repo.pulls.merged_info_text" (HTMLFormat "<code>%s</code>" .HeadTarget)}} </div> </div> <div class="item-section-right"> @@ -47,9 +48,9 @@ </div> {{end}} {{else if .Issue.IsClosed}} - <div class="item item-section text gt-f1"> + <div class="item item-section text tw-flex-1"> <div class="item-section-left"> - <h3 class="gt-mb-3">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3> + <h3 class="tw-mb-2">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3> <div class="merge-section-info"> {{if .IsPullRequestBroken}} {{ctx.Locale.Tr "repo.pulls.cant_reopen_deleted_branch"}} @@ -80,14 +81,14 @@ {{ctx.Locale.Tr "repo.pulls.data_broken"}} </div> {{else if .IsPullWorkInProgress}} - <div class="item toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{(.WorkInProgressPrefix|Escape)}}" data-update-url="{{.Issue.Link}}/title"> - <div class="item-section-left flex-text-inline gt-f1"> + <div class="item toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title"> + <div class="item-section-left flex-text-inline tw-flex-1"> {{svg "octicon-x"}} {{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}} </div> {{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}} <button class="ui compact button"> - {{ctx.Locale.Tr "repo.pulls.remove_prefix" (.WorkInProgressPrefix|Escape) | Safe}} + {{ctx.Locale.Tr "repo.pulls.remove_prefix" .WorkInProgressPrefix}} </button> {{end}} </div> @@ -126,7 +127,7 @@ {{else if .IsBlockedByChangedProtectedFiles}} <div class="item"> {{svg "octicon-x"}} - {{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n" | Safe}} + {{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}} </div> <ul> {{range .ChangedProtectedFiles}} @@ -197,7 +198,7 @@ {{if .AllowMerge}} {{/* user is allowed to merge */}} {{$prUnit := .Repository.MustGetUnit $.Context $.UnitTypePullRequests}} {{$approvers := (.Issue.PullRequest.GetApprovers ctx)}} - {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} + {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash $prUnit.PullRequestsConfig.AllowFastForwardOnly}} {{$hasPendingPullRequestMergeTip := ""}} {{if .HasPendingPullRequestMerge}} {{$createdPRMergeStr := TimeSinceUnix .PendingPullRequestMerge.CreatedUnix ctx.Locale}} @@ -268,6 +269,13 @@ 'mergeMessageFieldText': {{.GetCommitMessages}} + defaultSquashMergeMessage, 'hideAutoMerge': generalHideAutoMerge, }, + { + 'name': 'fast-forward-only', + 'allowed': {{and $prUnit.PullRequestsConfig.AllowFastForwardOnly (eq .Issue.PullRequest.CommitsBehind 0)}}, + 'textDoMerge': {{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}, + 'hideMergeMessageTexts': true, + 'hideAutoMerge': generalHideAutoMerge, + }, { 'name': 'manually-merged', 'allowed': {{$prUnit.PullRequestsConfig.AllowManualMerge}}, @@ -326,7 +334,7 @@ {{else if .IsBlockedByChangedProtectedFiles}} <div class="item text red"> {{svg "octicon-x"}} - {{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n" | Safe}} + {{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}} </div> <ul> {{range .ChangedProtectedFiles}} @@ -366,17 +374,15 @@ */}} {{if and $.StillCanManualMerge (not $showGeneralMergeForm)}} <div class="divider"></div> - <div class="ui form"> - <form action="{{.Link}}/merge" method="post"> - {{.CsrfTokenHtml}} - <div class="field"> - <input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}"> - </div> - <button class="ui red button" type="submit" name="do" value="manually-merged"> - {{ctx.Locale.Tr "repo.pulls.merge_manually"}} - </button> - </form> - </div> + <form class="ui form form-fetch-action" action="{{.Link}}/merge" method="post">{{/* another similar form is in PullRequestMergeForm.vue*/}} + {{.CsrfTokenHtml}} + <div class="field"> + <input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}"> + </div> + <button class="ui red button" type="submit" name="do" value="manually-merged"> + {{ctx.Locale.Tr "repo.pulls.merge_manually"}} + </button> + </form> {{end}} {{if and .Issue.PullRequest.HeadRepo (not .Issue.PullRequest.HasMerged) (not .Issue.IsClosed)}} diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl index 3dab44710e..d585d36574 100644 --- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl +++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl @@ -1,6 +1,6 @@ <div class="divider"></div> -<div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint" | Safe}} </div> -<div class="instruct-content gt-mt-3 gt-hidden"> +<div class="instruct-toggle"> {{ctx.Locale.Tr "repo.pulls.cmd_instruction_hint"}} </div> +<div class="instruct-content tw-mt-2 tw-hidden"> <div><h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_title"}}</h3>{{ctx.Locale.Tr "repo.pulls.cmd_instruction_checkout_desc"}}</div> {{$localBranch := .PullRequest.HeadBranch}} {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}} @@ -8,7 +8,7 @@ {{end}} <div class="ui secondary segment"> {{if eq .PullRequest.Flow 0}} - <div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}<gitea-origin-url data-url="{{.PullRequest.HeadRepo.Link}}"></gitea-origin-url>{{else}}origin{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div> + <div>git fetch -u {{if ne .PullRequest.HeadRepo.ID .PullRequest.BaseRepo.ID}}<origin-url data-url="{{.PullRequest.HeadRepo.Link}}"></origin-url>{{else}}origin{{end}} {{.PullRequest.HeadBranch}}:{{$localBranch}}</div> <div>git checkout {{$localBranch}}</div> {{else}} <div>git fetch -u origin {{.GetGitRefName}}:{{$localBranch}}</div> @@ -21,21 +21,25 @@ <div>git checkout {{.PullRequest.BaseBranch}}</div> <div>git merge --no-ff {{$localBranch}}</div> </div> - <div class="gt-hidden" data-pull-merge-style="rebase"> + <div class="tw-hidden" data-pull-merge-style="rebase"> <div>git checkout {{.PullRequest.BaseBranch}}</div> <div>git merge --ff-only {{$localBranch}}</div> </div> - <div class="gt-hidden" data-pull-merge-style="rebase-merge"> + <div class="tw-hidden" data-pull-merge-style="rebase-merge"> <div>git checkout {{$localBranch}}</div> <div>git rebase {{.PullRequest.BaseBranch}}</div> <div>git checkout {{.PullRequest.BaseBranch}}</div> <div>git merge --no-ff {{$localBranch}}</div> </div> - <div class="gt-hidden" data-pull-merge-style="squash"> + <div class="tw-hidden" data-pull-merge-style="squash"> <div>git checkout {{.PullRequest.BaseBranch}}</div> <div>git merge --squash {{$localBranch}}</div> </div> - <div class="gt-hidden" data-pull-merge-style="manually-merged"> + <div class="tw-hidden" data-pull-merge-style="fast-forward-only"> + <div>git checkout {{.PullRequest.BaseBranch}}</div> + <div>git merge --ff-only {{$localBranch}}</div> + </div> + <div class="tw-hidden" data-pull-merge-style="manually-merged"> <div>git checkout {{.PullRequest.BaseBranch}}</div> <div>git merge {{$localBranch}}</div> </div> diff --git a/templates/repo/issue/view_content/reference_issue_dialog.tmpl b/templates/repo/issue/view_content/reference_issue_dialog.tmpl index b771e08909..5f338f6768 100644 --- a/templates/repo/issue/view_content/reference_issue_dialog.tmpl +++ b/templates/repo/issue/view_content/reference_issue_dialog.tmpl @@ -2,7 +2,7 @@ <div class="header"> {{ctx.Locale.Tr "repo.issues.context.reference_issue"}} </div> - <div class="content" style="text-align:left"> + <div class="content tw-text-left"> <form class="ui form form-fetch-action" action="{{printf "%s/issues/new" .Repository.Link}}" method="post"> {{.CsrfTokenHtml}} <div class="ui segment content"> diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 6c13eef023..7040c2849a 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -2,11 +2,11 @@ {{template "repo/issue/branch_selector_field" .}} {{if .Issue.IsPull}} <input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}"> - <div class="ui {{if or (not .Reviewers) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown"> - <a class="text gt-df gt-ac muted"> + <div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown"> + <a class="text tw-flex tw-items-center muted"> <strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if and .CanChooseReviewer (not .Repository.IsArchived)}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </a> <div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/request_review"> @@ -20,22 +20,24 @@ {{range .Reviewers}} {{if .User}} <a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_{{.ItemID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}> - <span class="octicon-check {{if not .Checked}}gt-invisible{{end}}">{{svg "octicon-check"}}</span> + <span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span> <span class="text"> - {{ctx.AvatarUtils.Avatar .User 28 "gt-mr-3"}}{{template "repo/search_name" .User}} + {{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{template "repo/search_name" .User}} </span> </a> {{end}} {{end}} {{end}} {{if .TeamReviewers}} - <div class="divider"></div> + {{if .Reviewers}} + <div class="divider"></div> + {{end}} {{range .TeamReviewers}} {{if .Team}} <a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}> - <span class="octicon-check {{if not .Checked}}gt-invisible{{end}}">{{svg "octicon-check" 16}}</span> + <span class="octicon-check {{if not .Checked}}tw-invisible{{end}}">{{svg "octicon-check" 16}}</span> <span class="text"> - {{svg "octicon-people" 16 "gt-ml-4 gt-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}} + {{svg "octicon-people" 16 "tw-ml-4 tw-mr-1"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}} </span> </a> {{end}} @@ -45,20 +47,20 @@ </div> <div class="ui assignees list"> - <span class="no-select item {{if or .OriginalReviews .PullReviewers}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span> + <span class="no-select item {{if or .OriginalReviews .PullReviewers}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_reviewers"}}</span> <div class="selected"> {{range .PullReviewers}} - <div class="item gt-df gt-ac gt-py-3"> - <div class="gt-df gt-ac gt-f1"> + <div class="item tw-flex tw-items-center tw-py-2"> + <div class="tw-flex tw-items-center tw-flex-1"> {{if .User}} - <a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "gt-mr-3"}}{{.User.GetDisplayName}}</a> + <a class="muted sidebar-item-link" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20 "tw-mr-2"}}{{.User.GetDisplayName}}</a> {{else if .Team}} - <span class="text">{{svg "octicon-people" 20 "gt-mr-3"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span> + <span class="text">{{svg "octicon-people" 20 "tw-mr-2"}}{{$.Issue.Repo.OwnerName}}/{{.Team.Name}}</span> {{end}} </div> - <div class="gt-df gt-ac gt-gap-3"> - {{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed))}} - <a href="#" class="ui muted icon gt-df gt-ac show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}"> + <div class="tw-flex tw-items-center tw-gap-2"> + {{if (and $.Permission.IsAdmin (or (eq .Review.Type 1) (eq .Review.Type 3)) (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged))}} + <a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}"> {{svg "octicon-x" 20}} </a> <div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}"> @@ -89,7 +91,7 @@ {{svg "octicon-hourglass" 16}} </span> {{end}} - {{if .CanChange}} + {{if and .CanChange (or .Checked (and (not $.Issue.IsClosed) (not $.Issue.PullRequest.HasMerged)))}} <a href="#" class="ui muted icon re-request-review{{if .Checked}} checked{{end}}" data-tooltip-content="{{if .Checked}}{{ctx.Locale.Tr "repo.issues.remove_request_review"}}{{else}}{{ctx.Locale.Tr "repo.issues.re_request_review"}}{{end}}" data-issue-id="{{$.Issue.ID}}" data-id="{{.ItemID}}" data-update-url="{{$.RepoLink}}/issues/request_review">{{if .Checked}}{{svg "octicon-trash"}}{{else}}{{svg "octicon-sync"}}{{end}}</a> {{end}} {{svg (printf "octicon-%s" .Review.Type.Icon) 16 (printf "text %s" (.Review.HTMLTypeColorName))}} @@ -97,14 +99,14 @@ </div> {{end}} {{range .OriginalReviews}} - <div class="item gt-df gt-ac gt-py-3"> - <div class="gt-df gt-ac gt-f1"> - <a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" ($.Repository.GetOriginalURLHostname|Escape) | Safe}}"> - {{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "gt-mr-3"}} + <div class="item tw-flex tw-items-center tw-py-2"> + <div class="tw-flex tw-items-center tw-flex-1"> + <a class="muted" href="{{$.Repository.OriginalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $.Repository.GetOriginalURLHostname}}"> + {{svg (MigrationIcon $.Repository.GetOriginalURLHostname) 20 "tw-mr-2"}} {{.OriginalAuthor}} </a> </div> - <div class="gt-df gt-ac gt-gap-3"> + <div class="tw-flex tw-items-center tw-gap-2"> {{svg (printf "octicon-%s" .Type.Icon) 16 (printf "text %s" (.HTMLTypeColorName))}} </div> </div> @@ -112,9 +114,9 @@ </div> </div> {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}} - <div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{(index .PullRequestWorkInProgressPrefixes 0| Escape)}}" data-update-url="{{.Issue.Link}}/title"> + <div class="toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title"> <a class="muted"> - {{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}} + {{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}} </a> </div> {{end}} @@ -130,7 +132,7 @@ <a class="text muted flex-text-block"> <strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </a> <div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone"> @@ -138,11 +140,11 @@ </div> </div> <div class="ui select-milestone list"> - <span class="no-select item {{if .Issue.Milestone}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span> + <span class="no-select item {{if .Issue.Milestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span> <div class="selected"> {{if .Issue.Milestone}} <a class="item muted sidebar-item-link" href="{{.RepoLink}}/milestone/{{.Issue.Milestone.ID}}"> - {{svg "octicon-milestone" 18 "gt-mr-3"}} + {{svg "octicon-milestone" 18 "tw-mr-2"}} {{.Issue.Milestone.Name}} </a> {{end}} @@ -156,7 +158,7 @@ <a class="text muted flex-text-block"> <strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </a> <div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/projects"> @@ -174,7 +176,7 @@ </div> {{range .OpenProjects}} <a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}"> - {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </a> {{end}} {{end}} @@ -185,18 +187,18 @@ </div> {{range .ClosedProjects}} <a class="item muted sidebar-item-link" data-id="{{.ID}}" data-href="{{.Link ctx}}"> - {{svg .IconName 18 "gt-mr-3"}}{{.Title}} + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} </a> {{end}} {{end}} </div> </div> <div class="ui select-project list"> - <span class="no-select item {{if .Issue.Project}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span> + <span class="no-select item {{if .Issue.Project}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span> <div class="selected"> {{if .Issue.Project}} <a class="item muted sidebar-item-link" href="{{.Issue.Project.Link ctx}}"> - {{svg .Issue.Project.IconName 18 "gt-mr-3"}}{{.Issue.Project.Title}} + {{svg .Issue.Project.IconName 18 "tw-mr-2"}}{{.Issue.Project.Title}} </a> {{end}} </div> @@ -210,7 +212,7 @@ <a class="text muted flex-text-block"> <strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong> {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} - {{svg "octicon-gear" 16 "gt-ml-2"}} + {{svg "octicon-gear" 16 "tw-ml-1"}} {{end}} </a> <div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee"> @@ -229,21 +231,21 @@ {{$checked = true}} {{end}} {{end}} - <span class="octicon-check {{if not $checked}}gt-invisible{{end}}">{{svg "octicon-check"}}</span> + <span class="octicon-check {{if not $checked}}tw-invisible{{end}}">{{svg "octicon-check"}}</span> <span class="text"> - {{ctx.AvatarUtils.Avatar . 20 "gt-mr-3"}}{{template "repo/search_name" .}} + {{ctx.AvatarUtils.Avatar . 20 "tw-mr-2"}}{{template "repo/search_name" .}} </span> </a> {{end}} </div> </div> <div class="ui assignees list"> - <span class="no-select item {{if .Issue.Assignees}}gt-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span> + <span class="no-select item {{if .Issue.Assignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span> <div class="selected"> {{range .Issue.Assignees}} <div class="item"> <a class="muted sidebar-item-link" href="{{$.RepoLink}}/{{if $.Issue.IsPull}}pulls{{else}}issues{{end}}?assignee={{.ID}}"> - {{ctx.AvatarUtils.Avatar . 28 "gt-mr-3"}} + {{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}} {{.GetDisplayName}} </a> </div> @@ -255,10 +257,10 @@ {{if .Participants}} <span class="text"><strong>{{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}}</strong></span> - <div class="ui list gt-df gt-fw"> + <div class="ui list tw-flex tw-flex-wrap"> {{range .Participants}} <a {{if gt .ID 0}}href="{{.HomeLink}}"{{end}} data-tooltip-content="{{.GetDisplayName}}"> - {{ctx.AvatarUtils.Avatar . 28 "gt-my-1 gt-mr-2"}} + {{ctx.AvatarUtils.Avatar . 28 "tw-my-0.5 tw-mr-1"}} </a> {{end}} </div> @@ -269,7 +271,7 @@ <div class="ui watching"> <span class="text"><strong>{{ctx.Locale.Tr "notification.notifications"}}</strong></span> - <div class="gt-mt-3"> + <div class="tw-mt-2"> {{template "repo/issue/view_content/watching" .}} </div> </div> @@ -279,7 +281,7 @@ <div class="divider"></div> <div class="ui timetrack"> <span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span> - <div class="gt-mt-3"> + <div class="tw-mt-2"> <form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form"> {{$.CsrfTokenHtml}} </form> @@ -288,27 +290,27 @@ </form> {{if $.IsStopwatchRunning}} <button class="ui fluid button issue-stop-time"> - {{svg "octicon-stopwatch" 16 "gt-mr-3"}} + {{svg "octicon-stopwatch" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.stop_tracking"}} </button> - <button class="ui fluid button issue-cancel-time gt-mt-3"> - {{svg "octicon-trash" 16 "gt-mr-3"}} + <button class="ui fluid button issue-cancel-time tw-mt-2"> + {{svg "octicon-trash" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.cancel_tracking"}} </button> {{else}} {{if .HasUserStopwatch}} <div class="ui warning message"> - {{ctx.Locale.Tr "repo.issues.tracking_already_started" (.OtherStopwatchURL|Escape) | Safe}} + {{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}} </div> {{end}} <button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'> - {{svg "octicon-stopwatch" 16 "gt-mr-3"}} + {{svg "octicon-stopwatch" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.start_tracking_short"}} </button> <div class="ui mini modal issue-start-time-modal"> <div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div> <div class="content"> - <form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid gt-gap-3"> + <form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2"> {{$.CsrfTokenHtml}} <input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours"> <input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact"> @@ -319,8 +321,8 @@ <button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.add_time_cancel"}}</button> </div> </div> - <button class="ui fluid button issue-add-time gt-mt-3" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'> - {{svg "octicon-plus" 16 "gt-mr-3"}} + <button class="ui fluid button issue-add-time tw-mt-2" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'> + {{svg "octicon-plus" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.add_time_short"}} </button> {{end}} @@ -330,10 +332,10 @@ {{if .WorkingUsers}} <div class="divider"></div> <div class="ui comments"> - <span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time) | Safe}}</strong></span> + <span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span> <div> {{range $user, $trackedtime := .WorkingUsers}} - <div class="comment gt-mt-3"> + <div class="comment tw-mt-2"> <a class="avatar"> {{ctx.AvatarUtils.Avatar $user}} </a> @@ -353,20 +355,20 @@ <div class="divider"></div> <span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span> <div class="ui form" id="deadline-loader"> - <div class="ui negative message gt-hidden" id="deadline-err-invalid-date"> + <div class="ui negative message tw-hidden" id="deadline-err-invalid-date"> {{svg "octicon-x" 16 "close icon"}} {{ctx.Locale.Tr "repo.issues.due_date_invalid"}} </div> {{if ne .Issue.DeadlineUnix 0}} <p> - <div class="gt-df gt-sb gt-ac"> + <div class="tw-flex tw-justify-between tw-items-center"> <div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}> - {{svg "octicon-calendar" 16 "gt-mr-3"}} + {{svg "octicon-calendar" 16 "tw-mr-2"}} {{DateTime "long" .Issue.DeadlineUnix.FormatDate}} </div> <div> {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} - <a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil" 16 "gt-mr-2"}}</a> + <a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil" 16 "tw-mr-1"}}</a> <a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a> {{end}} </div> @@ -377,7 +379,7 @@ {{end}} {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} - <div {{if ne .Issue.DeadlineUnix 0}} class="gt-hidden"{{end}} id="deadlineForm"> + <div {{if ne .Issue.DeadlineUnix 0}} class="tw-hidden"{{end}} id="deadlineForm"> <form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" method="post" id="update-issue-deadline-form"> {{$.CsrfTokenHtml}} <input required placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}" {{if gt .Issue.DeadlineUnix 0}}value="{{.Issue.DeadlineUnix.FormatDate}}"{{end}} type="date" name="deadlineDate" id="deadlineDate"> @@ -415,8 +417,8 @@ </span> <div class="ui relaxed divided list"> {{range .BlockingDependencies}} - <div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb"> - <div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis"> + <div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between"> + <div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis"> <a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}"> #{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}} </a> @@ -424,7 +426,7 @@ {{.Repository.OwnerName}}/{{.Repository.Name}} </div> </div> - <div class="item-right gt-df gt-ac gt-m-2"> + <div class="item-right tw-flex tw-items-center tw-m-1"> {{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}} <a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}"> {{svg "octicon-trash" 16}} @@ -434,7 +436,7 @@ </div> {{end}} {{if .BlockingDependenciesNotPermitted}} - <div class="item gt-df gt-ac gt-sb gt-ellipsis"> + <div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis"> <span>{{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span> </div> {{end}} @@ -447,8 +449,8 @@ </span> <div class="ui relaxed divided list"> {{range .BlockedByDependencies}} - <div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb"> - <div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis"> + <div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between"> + <div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis"> <a class="title muted" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}"> #{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}} </a> @@ -456,7 +458,7 @@ {{.Repository.OwnerName}}/{{.Repository.Name}} </div> </div> - <div class="item-right gt-df gt-ac gt-m-2"> + <div class="item-right tw-flex tw-items-center tw-m-1"> {{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}} <a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blockedBy" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}"> {{svg "octicon-trash" 16}} @@ -467,8 +469,8 @@ {{end}} {{if $.CanCreateIssueDependencies}} {{range .BlockedByDependenciesNotPermitted}} - <div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb"> - <div class="item-left gt-df gt-jc gt-fc gt-f1 gt-ellipsis"> + <div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between"> + <div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis"> <div class="gt-ellipsis"> <span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span> <span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}"> @@ -479,7 +481,7 @@ {{.Repository.OwnerName}}/{{.Repository.Name}} </div> </div> - <div class="item-right gt-df gt-ac gt-m-2"> + <div class="item-right tw-flex tw-items-center tw-m-1"> {{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}} <a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.remove_info"}}"> {{svg "octicon-trash" 16}} @@ -489,7 +491,7 @@ </div> {{end}} {{else if .BlockedByDependenciesNotPermitted}} - <div class="item gt-df gt-ac gt-sb gt-ellipsis"> + <div class="item tw-flex tw-items-center tw-justify-between gt-ellipsis"> <span>{{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span> </div> {{end}} @@ -546,9 +548,9 @@ <div class="divider"></div> <div class="ui equal width compact grid"> {{$issueReferenceLink := printf "%s#%d" .Issue.Repo.FullName .Issue.Index}} - <div class="row gt-ac" data-tooltip-content="{{$issueReferenceLink}}"> + <div class="row tw-items-center" data-tooltip-content="{{$issueReferenceLink}}"> <span class="text column truncate">{{ctx.Locale.Tr "repo.issues.reference_link" $issueReferenceLink}}</span> - <button class="ui two wide button column gt-p-3" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button> + <button class="ui two wide button column tw-p-2" data-clipboard-text="{{$issueReferenceLink}}">{{svg "octicon-copy" 14}}</button> </div> </div> @@ -556,21 +558,21 @@ <div class="divider"></div> {{if or .PinEnabled .Issue.IsPinned}} - <form class="gt-mt-2 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}> + <form class="tw-mt-1 form-fetch-action single-button-form" method="post" {{if $.NewPinAllowed}}action="{{.Issue.Link}}/pin"{{else}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.max_pinned"}}"{{end}}> {{$.CsrfTokenHtml}} <button class="fluid ui button {{if not $.NewPinAllowed}}disabled{{end}}"> {{if not .Issue.IsPinned}} - {{svg "octicon-pin" 16 "gt-mr-3"}} + {{svg "octicon-pin" 16 "tw-mr-2"}} {{ctx.Locale.Tr "pin"}} {{else}} - {{svg "octicon-pin-slash" 16 "gt-mr-3"}} + {{svg "octicon-pin-slash" 16 "tw-mr-2"}} {{ctx.Locale.Tr "unpin"}} {{end}} </button> </form> {{end}} - <button class="gt-mt-2 fluid ui show-modal button {{if .Issue.IsLocked}} negative {{end}}" data-modal="#lock"> + <button class="tw-mt-1 fluid ui show-modal button {{if .Issue.IsLocked}} negative {{end}}" data-modal="#lock"> {{if .Issue.IsLocked}} {{svg "octicon-key"}} {{ctx.Locale.Tr "repo.issues.unlock"}} @@ -643,7 +645,7 @@ </form> </div> </div> - <button class="gt-mt-2 fluid ui show-modal button" data-modal="#sidebar-delete-issue"> + <button class="tw-mt-1 fluid ui show-modal button" data-modal="#sidebar-delete-issue"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.delete"}} </button> @@ -675,7 +677,7 @@ {{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}} <div class="divider"></div> <div class="inline field"> - <div class="ui checkbox" id="allow-edits-from-maintainers" + <div class="ui checkbox loading-icon-2px" id="allow-edits-from-maintainers" data-url="{{.Issue.Link}}" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_desc"}}" data-prompt-error="{{ctx.Locale.Tr "repo.pulls.allow_edits_from_maintainers_err"}}" diff --git a/templates/repo/issue/view_content/update_branch_by_merge.tmpl b/templates/repo/issue/view_content/update_branch_by_merge.tmpl index 4dbefefe00..adce052dee 100644 --- a/templates/repo/issue/view_content/update_branch_by_merge.tmpl +++ b/templates/repo/issue/view_content/update_branch_by_merge.tmpl @@ -7,7 +7,7 @@ </div> <div class="item-section-right"> {{if and $.UpdateAllowed $.UpdateByRebaseAllowed}} - <div class="gt-dib"> + <div class="tw-inline-block"> <div class="ui buttons update-button"> <button class="ui button" data-do="{{$.Link}}/update" data-redirect="{{$.Link}}"> <span class="button-text"> diff --git a/templates/repo/issue/view_content/watching.tmpl b/templates/repo/issue/view_content/watching.tmpl index 0e8562fed2..05936d090b 100644 --- a/templates/repo/issue/view_content/watching.tmpl +++ b/templates/repo/issue/view_content/watching.tmpl @@ -2,10 +2,10 @@ <input type="hidden" name="watch" value="{{if $.IssueWatch.IsWatching}}0{{else}}1{{end}}"> <button class="fluid ui button"> {{if $.IssueWatch.IsWatching}} - {{svg "octicon-mute" 16 "gt-mr-3"}} + {{svg "octicon-mute" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.unsubscribe"}} {{else}} - {{svg "octicon-unmute" 16 "gt-mr-3"}} + {{svg "octicon-unmute" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.subscribe"}} {{end}} </button> diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 7ec48c6734..b78ff55cda 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -1,5 +1,5 @@ {{if .Flash}} - <div class="sixteen wide column gt-mb-3"> + <div class="sixteen wide column tw-mb-2"> {{template "base/alert" .}} </div> {{end}} @@ -8,28 +8,28 @@ <h1 class="gt-word-break"> <span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span> </span> - <div id="edit-title-input" class="ui input gt-f1 gt-hidden"> + <div id="edit-title-input" class="ui input tw-flex-1 tw-hidden"> <input value="{{.Issue.Title}}" maxlength="255" autocomplete="off"> </div> </h1> <div class="issue-title-buttons"> {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} - <button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} gt-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button> + <button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button> {{end}} {{if not .Issue.IsPull}} - <a role="button" class="ui small primary button new-issue-button gt-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> + <a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> {{end}} </div> {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} <div class="edit-buttons"> - <button id="cancel-edit-title" class="ui small basic button in-edit gt-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> - <button id="save-edit-title" class="ui small primary button in-edit gt-hidden gt-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button> + <button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> + <button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button> </div> {{end}} </div> <div class="issue-title-meta"> {{if .HasMerged}} - <div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "gt-mr-2"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div> + <div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div> {{else if .Issue.IsClosed}} <div class="ui red label issue-state-label">{{if .Issue.IsPull}}{{svg "octicon-git-pull-request"}}{{else}}{{svg "octicon-issue-closed"}}{{end}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div> {{else if .Issue.IsPull}} @@ -41,43 +41,43 @@ {{else}} <div class="ui green label issue-state-label">{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.open_title"}}</div> {{end}} - <div class="gt-ml-3"> + <div class="tw-ml-2"> {{if .Issue.IsPull}} - {{$headHref := .HeadTarget|Escape}} + {{$headHref := .HeadTarget}} {{if .HeadBranchLink}} - {{$headHref = printf `<a href="%s">%s</a>` (.HeadBranchLink | Escape) $headHref}} + {{$headHref = HTMLFormat `<a href="%s">%s</a>` .HeadBranchLink $headHref}} {{end}} - {{$headHref = printf `%s <button class="btn interact-fg" data-tooltip-content="%s" data-clipboard-text="%s">%s</button>` $headHref (ctx.Locale.Tr "copy_branch") (.HeadTarget | Escape) (svg "octicon-copy" 14)}} - {{$baseHref := .BaseTarget|Escape}} + {{$headHref = HTMLFormat `%s <button class="btn interact-fg" data-tooltip-content="%s" data-clipboard-text="%s">%s</button>` $headHref (ctx.Locale.Tr "copy_branch") .HeadTarget (svg "octicon-copy" 14)}} + {{$baseHref := .BaseTarget}} {{if .BaseBranchLink}} - {{$baseHref = printf `<a href="%s">%s</a>` (.BaseBranchLink | Escape) $baseHref}} + {{$baseHref = HTMLFormat `<a href="%s">%s</a>` .BaseBranchLink $baseHref}} {{end}} {{if .Issue.PullRequest.HasMerged}} {{$mergedStr:= TimeSinceUnix .Issue.PullRequest.MergedUnix ctx.Locale}} {{if .Issue.OriginalAuthor}} {{.Issue.OriginalAuthor}} - <span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span> + <span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span> {{else}} <a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a> - <span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span> + <span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span> {{end}} {{else}} {{if .Issue.OriginalAuthor}} - <span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}}</span> + <span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span> {{else}} <span id="pull-desc" class="pull-desc"> <a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a> - {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}} + {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}} </span> {{end}} - <span id="pull-desc-edit" class="gt-hidden flex-text-block"> + <span id="pull-desc-edit" class="tw-hidden flex-text-block"> <div class="ui floating filter dropdown"> - <div class="ui basic small button gt-mr-0"> + <div class="ui basic small button tw-mr-0"> <span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span> </div> </div> {{svg "octicon-arrow-right"}} - <div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}"> + <div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}"> <div class="ui basic small button"> <span class="text" id="pull-target-branch" data-basename="{{$.BaseName}}" data-branch="{{$.BaseBranch}}">{{ctx.Locale.Tr "repo.pulls.compare_base"}}: {{$.BaseName}}:{{$.BaseBranch}}</span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} @@ -104,11 +104,11 @@ {{$createdStr:= TimeSinceUnix .Issue.CreatedUnix ctx.Locale}} <span class="time-desc"> {{if .Issue.OriginalAuthor}} - {{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.OriginalAuthor|Escape) | Safe}} + {{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.OriginalAuthor}} {{else if gt .Issue.Poster.ID 0}} - {{ctx.Locale.Tr "repo.issues.opened_by" $createdStr (.Issue.Poster.HomeLink|Escape) (.Issue.Poster.GetDisplayName|Escape) | Safe}} + {{ctx.Locale.Tr "repo.issues.opened_by" $createdStr .Issue.Poster.HomeLink .Issue.Poster.GetDisplayName}} {{else}} - {{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr (.Issue.Poster.GetDisplayName|Escape) | Safe}} + {{ctx.Locale.Tr "repo.issues.opened_by_fake" $createdStr .Issue.Poster.GetDisplayName}} {{end}} · {{ctx.Locale.TrN .Issue.NumComments "repo.issues.num_comments_1" "repo.issues.num_comments" .Issue.NumComments}} diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index b2f0798917..8bacb427bf 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -2,15 +2,15 @@ <div class="ui active tiny slow centered inline">…</div> {{else}} {{if .LatestCommitUser}} - {{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "gt-mr-2"}} - {{if .LatestCommitUser.FullName}} + {{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}} + {{if and .LatestCommitUser.FullName DefaultShowFullName}} <a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a> {{else}} <a class="muted author-wrapper" title="{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}</strong></a> {{end}} {{else}} {{if .LatestCommit.Author}} - {{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "gt-mr-2"}} + {{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "tw-mr-1"}} <span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span> {{end}} {{end}} @@ -25,7 +25,7 @@ <span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span> {{if IsMultilineCommitMessage .LatestCommit.Message}} <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button> - <pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre> + <pre class="commit-body tw-hidden">{{RenderCommitBody $.Context .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre> {{end}} </span> {{end}} diff --git a/templates/repo/migrate/codebase.tmpl b/templates/repo/migrate/codebase.tmpl index a34a039f8f..439a883863 100644 --- a/templates/repo/migrate/codebase.tmpl +++ b/templates/repo/migrate/codebase.tmpl @@ -35,22 +35,22 @@ <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> <div class="ui checkbox"> <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> <div class="ui checkbox"> <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> </div> </div> </div> @@ -90,10 +90,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl index 7fe4fbc672..db01b8d858 100644 --- a/templates/repo/migrate/git.tmpl +++ b/templates/repo/migrate/git.tmpl @@ -64,10 +64,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/gitbucket.tmpl b/templates/repo/migrate/gitbucket.tmpl index d07351e727..d1f1db99ba 100644 --- a/templates/repo/migrate/gitbucket.tmpl +++ b/templates/repo/migrate/gitbucket.tmpl @@ -34,7 +34,7 @@ <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> </div> </div> @@ -44,29 +44,29 @@ <label></label> <div class="ui checkbox"> <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> <div class="ui checkbox"> <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> <div class="ui checkbox"> <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> </div> @@ -106,10 +106,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/gitea.tmpl b/templates/repo/migrate/gitea.tmpl index a40886b7a5..143f220449 100644 --- a/templates/repo/migrate/gitea.tmpl +++ b/templates/repo/migrate/gitea.tmpl @@ -30,7 +30,7 @@ <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> <input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> </div> </div> @@ -40,29 +40,29 @@ <label></label> <div class="ui checkbox"> <input name="labels" type="checkbox" {{if .labels}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> <div class="ui checkbox"> <input name="issues" type="checkbox" {{if .issues}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> <div class="ui checkbox"> <input name="releases" type="checkbox" {{if .releases}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> </div> @@ -102,10 +102,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/github.tmpl b/templates/repo/migrate/github.tmpl index 07f8216fcb..dfb2b4bc46 100644 --- a/templates/repo/migrate/github.tmpl +++ b/templates/repo/migrate/github.tmpl @@ -33,7 +33,7 @@ <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> </div> </div> <div id="migrate_items"> @@ -42,29 +42,29 @@ <label></label> <div class="ui checkbox"> <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> <div class="ui checkbox"> <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> <div class="ui checkbox"> <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> </div> @@ -104,10 +104,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl index 623822df11..76c2828257 100644 --- a/templates/repo/migrate/gitlab.tmpl +++ b/templates/repo/migrate/gitlab.tmpl @@ -30,7 +30,7 @@ <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> <input name="wiki" type="checkbox" {{if .wiki}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> </div> </div> <div id="migrate_items"> @@ -39,29 +39,29 @@ <label></label> <div class="ui checkbox"> <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> <div class="ui checkbox"> <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> </div> <div class="ui checkbox"> <input name="releases" type="checkbox" {{if .releases}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> </div> @@ -101,10 +101,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/gogs.tmpl b/templates/repo/migrate/gogs.tmpl index 095efd5d60..b01d0eeb67 100644 --- a/templates/repo/migrate/gogs.tmpl +++ b/templates/repo/migrate/gogs.tmpl @@ -30,7 +30,7 @@ <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> <input name="wiki" type="checkbox" {{if .wiki}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_wiki" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_wiki"}}</label> </div> </div> @@ -40,18 +40,18 @@ <label></label> <div class="ui checkbox"> <input name="labels" type="checkbox" {{if .labels}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> <div class="ui checkbox"> <input name="issues" type="checkbox" {{if .issues}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="milestones" type="checkbox" {{if .milestones}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> </div> <!-- Gogs do not support it @@ -59,11 +59,11 @@ <label></label> <div class="ui checkbox"> <input name="pull_requests" type="checkbox" {{if .pull_requests}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_merge_requests"}}</label> </div> <div class="ui checkbox"> <input name="releases" type="checkbox" {{if .releases}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_releases" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_releases"}}</label> </div> </div> --> @@ -104,10 +104,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}} checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl index c686f0b832..c5c697edff 100644 --- a/templates/repo/migrate/migrate.tmpl +++ b/templates/repo/migrate/migrate.tmpl @@ -5,22 +5,22 @@ {{template "repo/migrate/helper" .}} <div class="ui cards migrate-entries"> {{range .Services}} - <a class="ui card migrate-entry gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}"> + <a class="ui card migrate-entry tw-flex tw-items-center" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}"> {{if eq .Name "github"}} - {{svg "octicon-mark-github" 184 "gt-p-4"}} + {{svg "octicon-mark-github" 184 "tw-p-4"}} {{else if eq .Name "gitlab"}} - {{svg "gitea-gitlab" 184 "gt-p-4"}} + {{svg "gitea-gitlab" 184 "tw-p-4"}} {{else if eq .Name "gitbucket"}} - {{svg "gitea-gitbucket" 184 "gt-p-4"}} + {{svg "gitea-gitbucket" 184 "tw-p-4"}} {{else}} {{svg (printf "gitea-%s" .Name) 184}} {{end}} <div class="content"> - <div class="header gt-text-center"> + <div class="header tw-text-center"> {{.Title}} </div> - <div class="description gt-text-center"> - {{(printf "repo.migrate.%s.description" .Name) | ctx.Locale.Tr}} + <div class="description tw-text-center"> + {{ctx.Locale.Tr (printf "repo.migrate.%s.description" .Name)}} </div> </div> </a> diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl index 48411e2da2..ed03db2ebd 100644 --- a/templates/repo/migrate/migrating.tmpl +++ b/templates/repo/migrate/migrating.tmpl @@ -12,7 +12,7 @@ <img src="{{AssetUrlPrefix}}/img/loading.png"> </div> </div> - <div id="repo_migrating_failed_image" class="sixteen wide center aligned centered column gt-hidden"> + <div id="repo_migrating_failed_image" class="sixteen wide center aligned centered column tw-hidden"> <div> <img src="{{AssetUrlPrefix}}/img/failed.png"> </div> @@ -21,14 +21,14 @@ <div class="ui stackable middle very relaxed page grid"> <div class="sixteen wide center aligned centered column"> <div id="repo_migrating_progress"> - <p>{{ctx.Locale.Tr "repo.migrate.migrating" .CloneAddr | Safe}}</p> + <p>{{ctx.Locale.Tr "repo.migrate.migrating" .CloneAddr}}</p> <p id="repo_migrating_progress_message"></p> </div> - <div id="repo_migrating_failed" class="gt-hidden"> + <div id="repo_migrating_failed" class="tw-hidden"> {{if .CloneAddr}} - <p>{{ctx.Locale.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}</p> + <p>{{ctx.Locale.Tr "repo.migrate.migrating_failed" .CloneAddr}}</p> {{else}} - <p>{{ctx.Locale.Tr "repo.migrate.migrating_failed_no_addr" | Safe}}</p> + <p>{{ctx.Locale.Tr "repo.migrate.migrating_failed_no_addr"}}</p> {{end}} <p id="repo_migrating_failed_error"></p> </div> @@ -40,7 +40,7 @@ {{else}} <button class="ui basic show-modal button" data-modal="#cancel-repo-modal">{{ctx.Locale.Tr "cancel"}}</button> {{end}} - <button id="repo_migrating_retry" data-migrating-task-retry-url="{{.Link}}/settings/migrate/retry" class="ui basic button gt-hidden">{{ctx.Locale.Tr "retry"}}</button> + <button id="repo_migrating_retry" data-migrating-task-retry-url="{{.Link}}/settings/migrate/retry" class="ui basic button tw-hidden">{{ctx.Locale.Tr "retry"}}</button> </div> {{end}} </div> @@ -57,8 +57,8 @@ </div> <div class="content"> <div class="ui warning message"> - {{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br> - {{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName | Safe}} + {{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br> + {{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}} {{if .Repository.NumForks}}<br> {{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}} {{end}} diff --git a/templates/repo/migrate/onedev.tmpl b/templates/repo/migrate/onedev.tmpl index b06e6929a1..8b2a2d8730 100644 --- a/templates/repo/migrate/onedev.tmpl +++ b/templates/repo/migrate/onedev.tmpl @@ -35,22 +35,22 @@ <label>{{ctx.Locale.Tr "repo.migrate_items"}}</label> <div class="ui checkbox"> <input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_milestones" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_milestones"}}</label> </div> <div class="ui checkbox"> <input name="labels" type="checkbox" {{if .labels}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_labels" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_labels"}}</label> </div> </div> <div class="inline field"> <label></label> <div class="ui checkbox"> <input name="issues" type="checkbox" {{if .issues}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_issues" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_issues"}}</label> </div> <div class="ui checkbox"> <input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.migrate_items_pullrequests"}}</label> </div> </div> </div> @@ -90,10 +90,10 @@ <div class="ui checkbox"> {{if .IsForcedPrivate}} <input name="private" type="checkbox" checked readonly> - <label>{{ctx.Locale.Tr "repo.visibility_helper_forced" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</label> {{else}} <input name="private" type="checkbox" {{if .private}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> {{end}} </div> </div> diff --git a/templates/repo/migrate/options.tmpl b/templates/repo/migrate/options.tmpl index 1bc30b886d..8a46e5769b 100644 --- a/templates/repo/migrate/options.tmpl +++ b/templates/repo/migrate/options.tmpl @@ -14,10 +14,10 @@ <input id="lfs" name="lfs" type="checkbox" {{if .lfs}} checked{{end}}> <label>{{ctx.Locale.Tr "repo.migrate_options_lfs"}}</label> </div> - <span id="lfs_settings" class="gt-hidden">(<a id="lfs_settings_show" href="#">{{ctx.Locale.Tr "repo.settings.advanced_settings"}}</a>)</span> + <span id="lfs_settings" class="tw-hidden">(<a id="lfs_settings_show" href="#">{{ctx.Locale.Tr "repo.settings.advanced_settings"}}</a>)</span> </div> -<div id="lfs_endpoint" class="gt-hidden"> - <span class="help">{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | Str2html}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}</span> +<div id="lfs_endpoint" class="tw-hidden"> + <span class="help">{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}{{if .ContextUser.CanImportLocal}} {{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.description.local"}}{{end}}</span> <div class="inline field {{if .Err_LFSEndpoint}}error{{end}}"> <label>{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.label"}}</label> <input name="lfs_endpoint" value="{{.lfs_endpoint}}" placeholder="{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.placeholder"}}"> diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl new file mode 100644 index 0000000000..b2471dc17e --- /dev/null +++ b/templates/repo/navbar.tmpl @@ -0,0 +1,14 @@ +<div class="ui fluid vertical menu"> + <a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity"> + {{ctx.Locale.Tr "repo.activity.navbar.pulse"}} + </a> + <a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors"> + {{ctx.Locale.Tr "repo.activity.navbar.contributors"}} + </a> + <a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency"> + {{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}} + </a> + <a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits"> + {{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}} + </a> +</div> diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl index 377a7ff79f..05ad7264bf 100644 --- a/templates/repo/projects/view.tmpl +++ b/templates/repo/projects/view.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project"> {{template "repo/header" .}} <div class="ui container padded"> - <div class="gt-df gt-sb gt-ac gt-mb-4"> + <div class="tw-flex tw-justify-between tw-items-center tw-mb-4"> {{template "repo/issue/navbar" .}} <a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> </div> diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl index 94de4d78eb..2cf0a85fc8 100644 --- a/templates/repo/pulls/fork.tmpl +++ b/templates/repo/pulls/fork.tmpl @@ -37,7 +37,7 @@ <div class="inline field"> <label>{{ctx.Locale.Tr "repo.fork_from"}}</label> - <a href="{{.ForkRepo.Link}}" class="gt-dib">{{.ForkRepo.FullName}}</a> + <a href="{{.ForkRepo.Link}}" class="tw-inline-block">{{.ForkRepo.FullName}}</a> </div> <div class="inline required field {{if .Err_RepoName}}error{{end}}"> <label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label> @@ -47,7 +47,7 @@ <label>{{ctx.Locale.Tr "repo.visibility"}}</label> <div class="ui disabled checkbox"> <input type="checkbox" disabled {{if .IsPrivate}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label> </div> <span class="help">{{ctx.Locale.Tr "repo.fork_visibility_helper"}}</span> </div> diff --git a/templates/repo/pulls/status.tmpl b/templates/repo/pulls/status.tmpl index ae508b8fa4..e8636ba1b8 100644 --- a/templates/repo/pulls/status.tmpl +++ b/templates/repo/pulls/status.tmpl @@ -2,6 +2,7 @@ Template Attributes: * CommitStatus: summary of all commit status state * CommitStatuses: all commit status elements +* MissingRequiredChecks: commit check contexts that are required by branch protection but not present * ShowHideChecks: whether use a button to show/hide the checks * is_context_required: Used in pull request commit status check table */}} @@ -9,7 +10,7 @@ Template Attributes: {{if .CommitStatus}} <div class="commit-status-panel"> <div class="ui top attached header commit-status-header"> - {{if eq .CommitStatus.State "pending"}} + {{if or (eq .CommitStatus.State "pending") (.MissingRequiredChecks)}} {{ctx.Locale.Tr "repo.pulls.status_checking"}} {{else if eq .CommitStatus.State "success"}} {{ctx.Locale.Tr "repo.pulls.status_checks_success"}} @@ -46,6 +47,13 @@ Template Attributes: </div> </div> {{end}} + {{range .MissingRequiredChecks}} + <div class="commit-status-item"> + {{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}} + <div class="status-context gt-ellipsis">{{.}}</div> + <div class="ui label">{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}</div> + </div> + {{end}} </div> </div> {{end}} diff --git a/templates/repo/pulls/tab_menu.tmpl b/templates/repo/pulls/tab_menu.tmpl index 10bdfdb3de..c0e48928f9 100644 --- a/templates/repo/pulls/tab_menu.tmpl +++ b/templates/repo/pulls/tab_menu.tmpl @@ -15,7 +15,7 @@ {{ctx.Locale.Tr "repo.pulls.tab_files"}} <span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span> </a> - <span class="item gt-ml-auto gt-pr-0 gt-font-bold gt-df gt-ac gt-gap-3"> + <span class="item tw-ml-auto tw-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2"> <span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span> <span class="diff-stats-bar"> <div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div> diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl new file mode 100644 index 0000000000..cfb3ec1d3d --- /dev/null +++ b/templates/repo/pulse.tmpl @@ -0,0 +1,227 @@ +<h2 class="ui header activity-header"> + <span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span> + <!-- Period --> + <div class="ui floating dropdown jump filter"> + <div class="ui basic compact button"> + {{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + </div> + <div class="menu"> + <a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a> + <a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a> + <a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a> + <a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a> + <a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a> + <a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a> + <a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a> + </div> + </div> +</h2> + +{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}} +<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4> +<div class="ui attached segment two column grid"> + {{if .Permission.CanRead $.UnitTypePullRequests}} + <div class="column"> + {{if gt .Activity.ActivePRCount 0}} + <div class="stats-table"> + <a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a> + <a href="#proposed-pull-requests" class="table-cell tiny background green"></a> + </div> + {{else}} + <div class="stats-table"> + <a class="table-cell tiny background light grey"></a> + </div> + {{end}} + {{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount}} + </div> + {{end}} + {{if .Permission.CanRead $.UnitTypeIssues}} + <div class="column"> + {{if gt .Activity.ActiveIssueCount 0}} + <div class="stats-table"> + <a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a> + <a href="#new-issues" class="table-cell tiny background green"></a> + </div> + {{else}} + <div class="stats-table"> + <a class="table-cell tiny background light grey"></a> + </div> + {{end}} + {{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount}} + </div> + {{end}} +</div> +<div class="ui attached segment horizontal segments"> + {{if .Permission.CanRead $.UnitTypePullRequests}} + <a href="#merged-pull-requests" class="ui attached segment text center"> + <span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br> + {{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}} + </a> + <a href="#proposed-pull-requests" class="ui attached segment text center"> + <span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br> + {{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}} + </a> + {{end}} + {{if .Permission.CanRead $.UnitTypeIssues}} + <a href="#closed-issues" class="ui attached segment text center"> + <span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br> + {{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}} + </a> + <a href="#new-issues" class="ui attached segment text center"> + <span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br> + {{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}} + </a> + {{end}} +</div> +{{end}} + +{{if .Permission.CanRead $.UnitTypeCode}} + {{if eq .Activity.Code.CommitCountInAllBranches 0}} + <div class="ui center aligned segment"> + <h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4> + </div> + {{end}} + {{if gt .Activity.Code.CommitCountInAllBranches 0}} + <div class="ui attached segment horizontal segments"> + <div class="ui attached segment text"> + {{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}} + <strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong> + {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}} + <strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong> + {{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}} + <strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong> + {{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}} + {{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}} + <strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong> + {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}} + {{ctx.Locale.Tr "repo.activity.git_stats_additions"}} + <strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong> + {{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}} + <strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>. + </div> + <div class="ui attached segment"> + <div id="repo-activity-top-authors-chart"></div> + </div> + </div> + {{end}} +{{end}} + +{{if gt .Activity.PublishedReleaseCount 0}} + <h4 class="divider divider-text tw-normal-case" id="published-releases"> + {{svg "octicon-tag" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.activity.title.releases_published_by" + (ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount) + (ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount) + }} + </h4> + <div class="list"> + {{range .Activity.PublishedReleases}} + <p class="desc"> + <span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span> + {{.TagName}} + {{if not .IsTag}} + <a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> + {{end}} + {{TimeSinceUnix .CreatedUnix ctx.Locale}} + </p> + {{end}} + </div> +{{end}} + +{{if gt .Activity.MergedPRCount 0}} + <h4 class="divider divider-text tw-normal-case" id="merged-pull-requests"> + {{svg "octicon-git-pull-request" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.activity.title.prs_merged_by" + (ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount) + (ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount) + }} + </h4> + <div class="list"> + {{range .Activity.MergedPRs}} + <p class="desc"> + <span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span> + #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> + {{TimeSinceUnix .MergedUnix ctx.Locale}} + </p> + {{end}} + </div> +{{end}} + +{{if gt .Activity.OpenedPRCount 0}} + <h4 class="divider divider-text tw-normal-case" id="proposed-pull-requests"> + {{svg "octicon-git-branch" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.activity.title.prs_opened_by" + (ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount) + (ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount) + }} + </h4> + <div class="list"> + {{range .Activity.OpenedPRs}} + <p class="desc"> + <span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span> + #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> + {{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}} + </p> + {{end}} + </div> +{{end}} + +{{if gt .Activity.ClosedIssueCount 0}} + <h4 class="divider divider-text tw-normal-case" id="closed-issues"> + {{svg "octicon-issue-closed" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.activity.title.issues_closed_from" + (ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount) + (ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount) + }} + </h4> + <div class="list"> + {{range .Activity.ClosedIssues}} + <p class="desc"> + <span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span> + #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> + {{TimeSinceUnix .ClosedUnix ctx.Locale}} + </p> + {{end}} + </div> +{{end}} + +{{if gt .Activity.OpenedIssueCount 0}} + <h4 class="divider divider-text tw-normal-case" id="new-issues"> + {{svg "octicon-issue-opened" 16 "tw-mr-2"}} + {{ctx.Locale.Tr "repo.activity.title.issues_created_by" + (ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount) + (ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount) + }} + </h4> + <div class="list"> + {{range .Activity.OpenedIssues}} + <p class="desc"> + <span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span> + #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> + {{TimeSinceUnix .CreatedUnix ctx.Locale}} + </p> + {{end}} + </div> +{{end}} + +{{if gt .Activity.UnresolvedIssueCount 0}} + <h4 class="divider divider-text tw-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}"> + {{svg "octicon-comment-discussion" 16 "tw-mr-2"}} + {{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}} + </h4> + <div class="list"> + {{range .Activity.UnresolvedIssues}} + <p class="desc"> + <span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span> + #{{.Index}} + {{if .IsPull}} + <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> + {{else}} + <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a> + {{end}} + {{TimeSinceUnix .UpdatedUnix ctx.Locale}} + </p> + {{end}} + </div> +{{end}} diff --git a/templates/repo/recent_commits.tmpl b/templates/repo/recent_commits.tmpl new file mode 100644 index 0000000000..5c241d635c --- /dev/null +++ b/templates/repo/recent_commits.tmpl @@ -0,0 +1,9 @@ +{{if .Permission.CanRead $.UnitTypeCode}} + <div id="repo-recent-commits-chart" + data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.recent_commits.what")}}" + data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.recent_commits.what")}}" + data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}" + data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}" + > + </div> +{{end}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index fb2fce2950..3139022bb4 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -5,90 +5,90 @@ {{template "base/alert" .}} {{template "repo/release_tag_header" .}} <ul id="release-list"> - {{range $idx, $release := .Releases}} + {{range $idx, $info := .Releases}} + {{$release := $info.Release}} <li class="ui grid"> <div class="ui four wide column meta"> - <a class="muted" href="{{if not (and .Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "gt-mr-2"}}{{.TagName}}</a> - {{if and .Sha1 ($.Permission.CanRead $.UnitTypeCode)}} - <a class="muted gt-mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a> - {{template "repo/branch_dropdown" dict "root" $ "release" .}} - {{end}} + <a class="muted" href="{{if not (and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode))}}#{{else}}{{$.RepoLink}}/src/tag/{{$release.TagName | PathEscapeSegments}}{{end}}" rel="nofollow">{{svg "octicon-tag" 16 "tw-mr-1"}}{{$release.TagName}}</a> + {{if and $release.Sha1 ($.Permission.CanRead $.UnitTypeCode)}} + <a class="muted tw-font-mono" href="{{$.RepoLink}}/src/commit/{{$release.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha $release.Sha1}}</a> + {{template "repo/branch_dropdown" dict "root" $ "release" $release}} + {{end}} </div> <div class="ui twelve wide column detail"> - <div class="gt-df gt-ac gt-sb gt-fw gt-mb-3"> - <h4 class="release-list-title gt-word-break"> - <a href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{.Title}}</a> - {{if .IsDraft}} - <span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span> - {{else if .IsPrerelease}} - <span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span> - {{else}} - <span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span> - {{end}} - </h4> - <div> - {{if $.CanCreateRelease}} - <a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{.TagName | PathEscapeSegments}}" rel="nofollow"> - {{svg "octicon-pencil"}} + <div class="tw-flex tw-items-center tw-justify-between tw-flex-wrap tw-mb-2"> + <h4 class="release-list-title gt-word-break"> + {{if $.PageIsSingleTag}}{{$release.Title}}{{else}}<a href="{{$.RepoLink}}/releases/tag/{{$release.TagName | PathEscapeSegments}}">{{$release.Title}}</a>{{end}} + {{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}} + {{if $release.IsDraft}} + <span class="ui yellow label">{{ctx.Locale.Tr "repo.release.draft"}}</span> + {{else if $release.IsPrerelease}} + <span class="ui orange label">{{ctx.Locale.Tr "repo.release.prerelease"}}</span> + {{else if (not $release.IsTag)}} + <span class="ui green label">{{ctx.Locale.Tr "repo.release.stable"}}</span> + {{end}} + </h4> + <div> + {{if and $.CanCreateRelease (not $.PageIsSingleTag)}} + <a class="muted" data-tooltip-content="{{ctx.Locale.Tr "repo.release.edit"}}" href="{{$.RepoLink}}/releases/edit/{{$release.TagName | PathEscapeSegments}}" rel="nofollow"> + {{svg "octicon-pencil"}} + </a> + {{end}} + </div> + </div> + <p class="text grey"> + <span class="author"> + {{if $release.OriginalAuthor}} + {{svg (MigrationIcon $release.Repo.GetOriginalURLHostname) 20 "tw-mr-1"}}{{$release.OriginalAuthor}} + {{else if $release.Publisher}} + {{ctx.AvatarUtils.Avatar $release.Publisher 20 "tw-mr-1"}} + <a href="{{$release.Publisher.HomeLink}}">{{$release.Publisher.GetDisplayName}}</a> + {{else}} + Ghost + {{end}} + </span> + <span class="released"> + {{ctx.Locale.Tr "repo.released_this"}} + </span> + {{if $release.CreatedUnix}} + <span class="time">{{TimeSinceUnix $release.CreatedUnix ctx.Locale}}</span> + {{end}} + {{if and (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}} + | <span class="ahead"><a href="{{$.RepoLink}}/compare/{{$release.TagName | PathEscapeSegments}}...{{$release.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}}</span> + {{end}} + </p> + <div class="markup desc"> + {{$release.RenderedNote}} + </div> + <div class="divider"></div> + <details class="download" {{if eq $idx 0}}open{{end}}> + <summary class="tw-my-4"> + {{ctx.Locale.Tr "repo.release.downloads"}} + </summary> + <ul class="list"> + {{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead $.UnitTypeCode)}} + <li> + <a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a> + </li> + <li> + <a class="archive-link" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a> + </li> + {{end}} + {{range $release.Attachments}} + <li> + <a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download> + <strong>{{svg "octicon-package" 16 "tw-mr-1"}}{{.Name}}</strong> </a> - {{end}} - </div> - </div> - <p class="text grey"> - <span class="author"> - {{if .OriginalAuthor}} - {{svg (MigrationIcon .Repo.GetOriginalURLHostname) 20 "gt-mr-2"}}{{.OriginalAuthor}} - {{else if .Publisher}} - {{ctx.AvatarUtils.Avatar .Publisher 20 "gt-mr-2"}} - <a href="{{.Publisher.HomeLink}}">{{.Publisher.GetDisplayName}}</a> - {{else}} - Ghost + <div> + <span class="text grey">{{.Size | FileSize}}</span> + <span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}"> + {{svg "octicon-info"}} + </span> + </div> + </li> {{end}} - </span> - <span class="released"> - {{ctx.Locale.Tr "repo.released_this"}} - </span> - {{if .CreatedUnix}} - <span class="time">{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span> - {{end}} - {{if and (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}} - | <span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}...{{.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" .TargetBehind}}</span> - {{end}} - </p> - <div class="markup desc"> - {{Str2html .Note}} - </div> - <div class="divider"></div> - <details class="download" {{if eq $idx 0}}open{{end}}> - <summary class="gt-my-4"> - {{ctx.Locale.Tr "repo.release.downloads"}} - </summary> - <ul class="list"> - {{if and (not $.DisableDownloadSourceArchives) (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}} - <li> - <a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a> - </li> - <li> - <a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a> - </li> - {{end}} - {{if .Attachments}} - {{range .Attachments}} - <li> - <a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download> - <strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong> - </a> - <div> - <span class="text grey">{{.Size | FileSize}}</span> - <span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}"> - {{svg "octicon-info"}} - </span> - </div> - </li> - {{end}} - {{end}} - </ul> - </details> + </ul> + </details> <div class="dot"></div> </div> </li> diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index 46b1c9b291..c01f9a421b 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -21,7 +21,7 @@ {{else}} <input id="tag-name" name="tag_name" value="{{.tag_name}}" aria-label="{{ctx.Locale.Tr "repo.release.tag_name"}}" placeholder="{{ctx.Locale.Tr "repo.release.tag_name"}}" autofocus required maxlength="255"> <input id="tag-name-editor" type="hidden" data-existing-tags="{{JsonUtils.EncodeToString .Tags}}" data-tag-helper="{{ctx.Locale.Tr "repo.release.tag_helper"}}" data-tag-helper-new="{{ctx.Locale.Tr "repo.release.tag_helper_new"}}" data-tag-helper-existing="{{ctx.Locale.Tr "repo.release.tag_helper_existing"}}"> - <div id="tag-target-selector" class="gt-dib"> + <div id="tag-target-selector" class="tw-inline-block"> <span class="at">@</span> <div class="ui selection dropdown"> <input type="hidden" name="tag_target" value="{{.tag_target}}"> @@ -39,12 +39,12 @@ </div> </div> <div> - <span id="tag-helper" class="help gt-mt-3 gt-pb-0">{{ctx.Locale.Tr "repo.release.tag_helper"}}</span> + <span id="tag-helper" class="help tw-mt-2 tw-pb-0">{{ctx.Locale.Tr "repo.release.tag_helper"}}</span> </div> {{end}} </div> </div> - <div class="eleven wide gt-pt-0"> + <div class="eleven wide tw-pt-0"> <div class="field {{if .Err_Title}}error{{end}}"> <input name="title" aria-label="{{ctx.Locale.Tr "repo.release.title"}}" placeholder="{{ctx.Locale.Tr "repo.release.title"}}" value="{{.title}}" autofocus maxlength="255"> </div> @@ -61,10 +61,10 @@ </div> {{range .attachments}} <div class="field flex-text-block" id="attachment-{{.ID}}"> - <div class="flex-text-inline gt-f1"> + <div class="flex-text-inline tw-flex-1"> <input name="attachment-edit-{{.UUID}}" class="attachment_edit" required value="{{.Name}}"> <input name="attachment-del-{{.UUID}}" type="hidden" value="false"> - <span class="ui text grey gt-whitespace-nowrap">{{.Size | FileSize}}</span> + <span class="ui text grey tw-whitespace-nowrap">{{.Size | FileSize}}</span> <span data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber .DownloadCount)}}"> {{svg "octicon-info"}} </span> @@ -100,8 +100,8 @@ </div> </div> <span class="help">{{ctx.Locale.Tr "repo.release.prerelease_helper"}}</span> - <div class="divider gt-mt-0"></div> - <div class="gt-df gt-je"> + <div class="divider tw-mt-0"></div> + <div class="tw-flex tw-justify-end"> {{if .PageIsEditRelease}} <a class="ui small button" href="{{.RepoLink}}/releases"> {{ctx.Locale.Tr "repo.release.cancel"}} diff --git a/templates/repo/release_tag_header.tmpl b/templates/repo/release_tag_header.tmpl index f474fb89ea..ab1e58620d 100644 --- a/templates/repo/release_tag_header.tmpl +++ b/templates/repo/release_tag_header.tmpl @@ -2,22 +2,22 @@ {{$canReadCode := $.Permission.CanRead $.UnitTypeCode}} {{if $canReadReleases}} - <div class="gt-df"> - <div class="gt-f1 gt-df gt-ac"> - <h2 class="ui compact small menu header small-menu-items"> - <a class="{{if .PageIsReleaseList}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a> + <div class="tw-flex"> + <div class="tw-flex-1 tw-flex tw-items-center"> + <h2 class="ui compact small menu small-menu-items"> + <a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a> {{if $canReadCode}} - <a class="{{if .PageIsTagList}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a> + <a class="{{if or .PageIsTagList .PageIsSingleTag}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a> {{end}} </h2> </div> {{if .EnableFeed}} <a class="ui small button" href="{{.RepoLink}}/{{if .PageIsTagList}}tags{{else}}releases{{end}}.rss"> - {{svg "octicon-rss" 18}} {{ctx.Locale.Tr "rss_feed"}} + {{svg "octicon-rss" 16}} {{ctx.Locale.Tr "rss_feed"}} </a> {{end}} {{if and (not .PageIsTagList) .CanCreateRelease}} - <a class="ui small primary button" href="{{$.RepoLink}}/releases/new"> + <a class="ui small primary button" href="{{$.RepoLink}}/releases/new{{if .PageIsSingleTag}}?tag={{.TagName}}{{end}}"> {{ctx.Locale.Tr "repo.release.new_release"}} </a> {{end}} diff --git a/templates/repo/search.tmpl b/templates/repo/search.tmpl index b616b4de32..3f5b22b0ce 100644 --- a/templates/repo/search.tmpl +++ b/templates/repo/search.tmpl @@ -2,72 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository file list"> {{template "repo/header" .}} <div class="ui container"> - <div class="ui repo-search"> - <form class="ui form ignore-dirty" method="get"> - <div class="ui fluid action input"> - <input name="q" value="{{.Keyword}}"{{if .CodeIndexerUnavailable}} disabled{{end}} placeholder="{{ctx.Locale.Tr "repo.search.search_repo"}}"> - <div class="ui dropdown selection {{if .CodeIndexerUnavailable}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.search.type.tooltip"}}"> - <input name="t" type="hidden"{{if .CodeIndexerUnavailable}} disabled{{end}} value="{{.queryType}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}} - <div class="text">{{ctx.Locale.Tr (printf "repo.search.%s" (or .queryType "fuzzy"))}}</div> - <div class="menu"> - <div class="item" data-value="" data-tooltip-content="{{ctx.Locale.Tr "repo.search.fuzzy.tooltip"}}">{{ctx.Locale.Tr "repo.search.fuzzy"}}</div> - <div class="item" data-value="match" data-tooltip-content="{{ctx.Locale.Tr "repo.search.match.tooltip"}}">{{ctx.Locale.Tr "repo.search.match"}}</div> - </div> - </div> - <button class="ui icon button"{{if .CodeIndexerUnavailable}} disabled{{end}} type="submit">{{svg "octicon-search" 16}}</button> - </div> - </form> - </div> - {{if .CodeIndexerUnavailable}} - <div class="ui error message"> - <p>{{ctx.Locale.Tr "repo.search.code_search_unavailable"}}</p> - </div> - {{else if .Keyword}} - <h3> - {{ctx.Locale.Tr "repo.search.results" (.Keyword|Escape) (.RepoLink|Escape) (.RepoName|Escape) | Str2html}} - </h3> - {{if .SearchResults}} - <div class="flex-text-block gt-fw"> - {{range $term := .SearchResultLanguages}} - <a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label gt-m-0" href="{{$.SourcePath}}/search?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}{{if ne $.queryType ""}}&t={{$.queryType}}{{end}}"> - <i class="color-icon gt-mr-3" style="background-color: {{$term.Color}}"></i> - {{$term.Language}} - <div class="detail">{{$term.Count}}</div> - </a> - {{end}} - </div> - <div class="repository search"> - {{range $result := .SearchResults}} - <div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result"> - <h4 class="ui top attached normal header gt-df gt-fw"> - <span class="file gt-f1">{{.Filename}}</span> - <a role="button" class="ui basic tiny button" rel="nofollow" href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments .Filename}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> - </h4> - <div class="ui attached table segment"> - <div class="file-body file-code code-view"> - <table> - <tbody> - <tr> - <td class="lines-num"> - {{range .LineNumbers}} - <a href="{{$.SourcePath}}/src/commit/{{PathEscape $result.CommitID}}/{{PathEscapeSegments $result.Filename}}#L{{.}}"><span>{{.}}</span></a> - {{end}} - </td> - <td class="lines-code chroma"><code class="code-inner">{{.FormattedLines}}</code></td> - </tr> - </tbody> - </table> - </div> - </div> - {{template "shared/searchbottom" dict "root" $ "result" .}} - </div> - {{end}} - </div> - {{template "base/paginate" .}} - {{else}} - <div>{{ctx.Locale.Tr "repo.search.code_no_results"}}</div> - {{end}} - {{end}} + {{template "shared/search/code/search" .}} </div> </div> {{template "base/footer" .}} diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl index fbdc12defb..52c0c2c800 100644 --- a/templates/repo/settings/branches.tmpl +++ b/templates/repo/settings/branches.tmpl @@ -1,7 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}} <div class="repo-setting-content"> {{if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{ctx.Locale.Tr "repo.settings.archive.branchsettings_unavailable"}} </div> {{else}} @@ -12,11 +12,11 @@ <p> {{ctx.Locale.Tr "repo.settings.default_branch_desc"}} </p> - <form class="gt-df" action="{{.Link}}" method="post"> + <form class="tw-flex" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <input type="hidden" name="action" value="default_branch"> {{if not .Repository.IsEmpty}} - <div class="ui dropdown selection gt-f1 gt-mr-3 gt-max-width-24rem"> + <div class="ui dropdown selection search tw-flex-1 tw-mr-2 tw-max-w-96"> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <input type="hidden" name="branch" value="{{.Repository.DefaultBranch}}"> <div class="default text">{{.Repository.DefaultBranch}}</div> @@ -41,7 +41,7 @@ <div class="ui attached segment"> <div class="flex-list"> {{range .ProtectedBranches}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-main"> <div class="flex-item-title"> <div class="ui basic primary label">{{.RuleName}}</div> diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl index 19abf1bee8..2a4ec577e7 100644 --- a/templates/repo/settings/collaboration.tmpl +++ b/templates/repo/settings/collaboration.tmpl @@ -7,7 +7,7 @@ <div class="ui attached segment"> <div class="flex-list"> {{range .Collaborators}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> <a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a> </div> @@ -41,8 +41,8 @@ <div class="ui bottom attached segment"> <form class="ui form" id="repo-collab-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <div id="search-user-box" class="ui search input gt-vm"> - <input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" autofocus required> + <div id="search-user-box" class="ui search input tw-align-middle"> + <input class="prompt" name="collaborator" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required> </div> <button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_collaborator"}}</button> </form> @@ -89,8 +89,8 @@ {{if $allowedToChangeTeams}} <form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post"> {{.CsrfTokenHtml}} - <div id="search-team-box" class="ui search input gt-vm" data-org-name="{{.OrgName}}"> - <input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "repo.settings.search_team"}}" autocomplete="off" autofocus required> + <div id="search-team-box" class="ui search input tw-align-middle" data-org-name="{{.OrgName}}"> + <input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required> </div> <button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_team"}}</button> </form> diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl index c5d2d2a04a..da1a321785 100644 --- a/templates/repo/settings/deploy_keys.tmpl +++ b/templates/repo/settings/deploy_keys.tmpl @@ -11,18 +11,18 @@ </div> </h4> <div class="ui attached segment"> - <div class="{{if not .HasError}}gt-hidden{{end}} gt-mb-4" id="add-deploy-key-panel"> + <div class="{{if not .HasError}}tw-hidden{{end}} tw-mb-4" id="add-deploy-key-panel"> <form class="ui form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <div class="field"> {{ctx.Locale.Tr "repo.settings.deploy_key_desc"}} </div> <div class="field {{if .Err_Title}}error{{end}}"> - <label for="title">{{ctx.Locale.Tr "repo.settings.title"}}</label> + <label for="ssh-key-title">{{ctx.Locale.Tr "repo.settings.title"}}</label> <input id="ssh-key-title" name="title" value="{{.title}}" autofocus required> </div> <div class="field {{if .Err_Content}}error{{end}}"> - <label for="content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label> + <label for="ssh-key-content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label> <textarea id="ssh-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea> </div> <div class="field"> @@ -31,7 +31,7 @@ <label for="is_writable"> {{ctx.Locale.Tr "repo.settings.is_writable"}} </label> - <small style="padding-left: 26px;">{{ctx.Locale.Tr "repo.settings.is_writable_info" | Str2html}}</small> + <small class="tw-pl-[26px]">{{ctx.Locale.Tr "repo.settings.is_writable_info"}}</small> </div> </div> <button class="ui primary button"> @@ -55,7 +55,7 @@ {{.Fingerprint}} </div> <div class="flex-item-body"> - <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}} - <span>{{ctx.Locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{ctx.Locale.Tr "settings.can_write_info"}} {{end}}</span></i> + <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}} - <span>{{ctx.Locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{ctx.Locale.Tr "settings.can_write_info"}} {{end}}</span></i> </div> </div> <div class="flex-item-trailing"> diff --git a/templates/repo/settings/githook_edit.tmpl b/templates/repo/settings/githook_edit.tmpl index db8982a282..e20f51b922 100644 --- a/templates/repo/settings/githook_edit.tmpl +++ b/templates/repo/settings/githook_edit.tmpl @@ -14,7 +14,7 @@ </div> <div class="field"> <label for="content">{{ctx.Locale.Tr "repo.settings.githook_content"}}</label> - <textarea id="content" name="content" class="gt-hidden">{{if .IsActive}}{{.Content}}{{else}}{{.Sample}}{{end}}</textarea> + <textarea id="content" name="content" class="tw-hidden">{{if .IsActive}}{{.Content}}{{else}}{{.Sample}}{{end}}</textarea> <div class="editor-loading is-loading"></div> </div> <div class="inline field"> diff --git a/templates/repo/settings/githooks.tmpl b/templates/repo/settings/githooks.tmpl index 389d381f30..1a603f9fe8 100644 --- a/templates/repo/settings/githooks.tmpl +++ b/templates/repo/settings/githooks.tmpl @@ -6,13 +6,13 @@ <div class="ui attached segment"> <div class="ui list"> <div class="item"> - {{ctx.Locale.Tr "repo.settings.githooks_desc" | Str2html}} + {{ctx.Locale.Tr "repo.settings.githooks_desc"}} </div> {{range .Hooks}} <div class="item truncated-item-container"> - <span class="text {{if .IsActive}}green{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span> - <span class="text truncate gt-f1 gt-mr-3">{{.Name}}</span> - <a class="muted gt-float-right gt-p-3" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}"> + <span class="text {{if .IsActive}}green{{else}}grey{{end}} tw-mr-2">{{svg "octicon-dot-fill" 22}}</span> + <span class="text truncate tw-flex-1 tw-mr-2">{{.Name}}</span> + <a class="muted tw-float-right tw-p-2" href="{{$.RepoLink}}/settings/hooks/git/{{.Name|PathEscape}}"> {{svg "octicon-pencil"}} </a> </div> diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl index dca4d1f1ce..e0864ff221 100644 --- a/templates/repo/settings/lfs.tmpl +++ b/templates/repo/settings/lfs.tmpl @@ -12,7 +12,7 @@ {{range .LFSFiles}} <tr> <td> - <a href="{{$.Link}}/show/{{.Oid}}" title="{{.Oid}}" class="ui brown button gt-mono"> + <a href="{{$.Link}}/show/{{.Oid}}" title="{{.Oid}}" class="ui brown button tw-font-mono"> {{ShortSha .Oid}} </a> </td> diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index 0aeb2af178..cb65236f23 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -5,7 +5,7 @@ <a href="{{.LFSFilesLink}}">{{ctx.Locale.Tr "repo.settings.lfs"}}</a> / <span class="truncate sha">{{.LFSFile.Oid}}</span> <div class="ui right"> {{if .EscapeStatus.Escaped}} - <a class="ui tiny basic button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a> + <a class="ui tiny basic button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a> <a class="ui tiny basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a> {{end}} <a class="ui primary tiny button" href="{{.LFSFilesLink}}/find?oid={{.LFSFile.Oid}}&size={{.LFSFile.Size}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a> @@ -14,10 +14,12 @@ <div class="ui attached table unstackable segment"> {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} <div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}"> - {{if .IsMarkup}} - {{if .FileContent}}{{.FileContent | Safe}}{{end}} + {{if .IsFileTooLarge}} + {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} + {{else if .IsMarkup}} + {{if .FileContent}}{{.FileContent | SafeHTML}}{{end}} {{else if .IsPlainText}} - <pre>{{if .FileContent}}{{.FileContent | Safe}}{{end}}</pre> + <pre>{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}</pre> {{else if not .IsTextFile}} <div class="view-raw"> {{if .IsImageFile}} @@ -33,19 +35,15 @@ {{else if .IsPDFFile}} <div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div> {{else}} - <a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> + <a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> {{end}} </div> {{else if .FileSize}} <table> <tbody> <tr> - {{if .IsFileTooLarge}} - <td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td> - {{else}} <td class="lines-num">{{.LineNums}}</td> <td class="lines-code"><pre><code class="{{.HighlightClass}}"><ol>{{.FileContent}}</ol></code></pre></td> - {{end}} </tr> </tbody> </table> diff --git a/templates/repo/settings/lfs_file_find.tmpl b/templates/repo/settings/lfs_file_find.tmpl index fea9aa323f..809a028b2c 100644 --- a/templates/repo/settings/lfs_file_find.tmpl +++ b/templates/repo/settings/lfs_file_find.tmpl @@ -14,7 +14,7 @@ </td> <td class="message"> <span class="truncate"> - <a href="{{$.RepoLink}}/commit/{{.SHA}}" title="{{.Summary | RenderEmojiPlain}}"> + <a href="{{$.RepoLink}}/commit/{{.SHA}}" title="{{.Summary}}"> {{.Summary | RenderEmoji $.Context}} </a> </span> diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl index fdc6b536c2..a0bb8c46f0 100644 --- a/templates/repo/settings/lfs_pointers.tmpl +++ b/templates/repo/settings/lfs_pointers.tmpl @@ -32,19 +32,19 @@ {{range .Pointers}} <tr> <td> - <a href="{{$.RepoLink}}/raw/blob/{{.SHA}}" rel="nofollow" target="_blank" title="{{.SHA}}" class="ui button gt-mono"> + <a href="{{$.RepoLink}}/raw/blob/{{.SHA}}" rel="nofollow" target="_blank" title="{{.SHA}}" class="ui button tw-font-mono"> {{ShortSha .SHA}} </a> </td> <td> - <a {{if and .Exists .InRepo}}href="{{$.LFSFilesLink}}/show/{{.Oid}}" rel="nofollow" target="_blank"{{end}} title="{{.Oid}}" class="ui brown button gt-mono"> + <a {{if and .Exists .InRepo}}href="{{$.LFSFilesLink}}/show/{{.Oid}}" rel="nofollow" target="_blank"{{end}} title="{{.Oid}}" class="ui brown button tw-font-mono"> {{ShortSha .Oid}} </a> </td> <td>{{if .InRepo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if .Exists}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if .Accessible}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> - <td class="gt-text-right"> + <td class="tw-text-right"> <a class="ui primary button" href="{{$.LFSFilesLink}}/find?oid={{.Oid}}&size={{.Size}}&sha={{.SHA}}">{{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}</a> </td> </tr> diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 3bef0fa4c1..0b0ef0b6e8 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -12,10 +12,12 @@ {{ctx.Locale.Tr "repo.settings.hooks"}} </a> {{end}} - {{if and (.Repository.UnitEnabled $.Context $.UnitTypeCode) (not .Repository.IsEmpty)}} - <a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches"> - {{ctx.Locale.Tr "repo.settings.branches"}} - </a> + {{if .Repository.UnitEnabled $.Context $.UnitTypeCode}} + {{if not .Repository.IsEmpty}} + <a class="{{if .PageIsSettingsBranches}}active {{end}}item" href="{{.RepoLink}}/settings/branches"> + {{ctx.Locale.Tr "repo.settings.branches"}} + </a> + {{end}} <a class="{{if .PageIsSettingsTags}}active {{end}}item" href="{{.RepoLink}}/settings/tags"> {{ctx.Locale.Tr "repo.settings.tags"}} </a> diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index dfb909e743..b8fa4759b1 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -32,7 +32,7 @@ {{else}} <input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}{{if and $.ForcePrivate .Repository.IsPrivate}} readonly{{end}}> {{end}} - <label>{{ctx.Locale.Tr "repo.visibility_helper" | Safe}} {{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</label> + <label>{{ctx.Locale.Tr "repo.visibility_helper"}} {{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</label> </div> </div> {{end}} @@ -80,7 +80,7 @@ </h4> <div class="ui attached segment"> {{if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{ctx.Locale.Tr "repo.settings.archive.mirrors_unavailable"}} </div> {{else}} @@ -110,19 +110,18 @@ <table class="ui table"> <thead> <tr> - <th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th> + <th class="tw-w-2/5">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th> <th>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}</th> <th>{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}</th> <th></th> </tr> </thead> - {{end}} {{if $modifyBrokenPullMirror}} {{/* even if a repo is a pull mirror (IsMirror=true), the PullMirror might still be nil if the mirror migration is broken */}} <tbody> <tr> <td colspan="4"> - <div class="text red gt-py-4 gt-border-secondary-bottom">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div> + <div class="text red tw-py-4">{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}: {{ctx.Locale.Tr "error.occurred"}}</div> </td> </tr> </tbody> @@ -133,7 +132,7 @@ <td>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction.pull"}}</td> <td>{{DateTime "full" .PullMirror.UpdatedUnix}}</td> <td class="right aligned"> - <form method="post" class="gt-dib"> + <form method="post" class="tw-inline-block"> {{.CsrfTokenHtml}} <input type="hidden" name="action" value="mirror-sync"> <button class="ui primary tiny button inline text-thin">{{ctx.Locale.Tr "repo.settings.sync_mirror"}}</button> @@ -164,10 +163,10 @@ <p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p> </div> <details class="ui optional field" {{if or .Err_Auth $address.Username}}open{{end}}> - <summary class="gt-p-2"> + <summary class="tw-p-1"> {{ctx.Locale.Tr "repo.need_auth"}} </summary> - <div class="gt-p-2"> + <div class="tw-p-1"> <div class="inline field {{if .Err_Auth}}error{{end}}"> <label for="mirror_username">{{ctx.Locale.Tr "username"}}</label> <input id="mirror_username" name="mirror_username" value="{{$address.Username}}" {{if not .mirror_username}}data-need-clear="true"{{end}}> @@ -191,7 +190,7 @@ <div class="field {{if .Err_LFSEndpoint}}error{{end}}"> <label for="mirror_lfs_endpoint">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint"}}</label> <input id="mirror_lfs_endpoint" name="mirror_lfs_endpoint" value="{{.PullMirror.LFSEndpoint}}" placeholder="{{ctx.Locale.Tr "repo.migrate_options_lfs_endpoint.placeholder"}}"> - <p class="help">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery" | Str2html}}</p> + <p class="help">{{ctx.Locale.Tr "repo.mirror_lfs_endpoint_desc" "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md#server-discovery"}}</p> </div> {{end}} <div class="field"> @@ -201,13 +200,14 @@ </td> </tr> </tbody> + {{end}}{{/* end if: $modifyBrokenPullMirror / $isWorkingPullMirror */}} </table> - {{end}}{{/* end if: IsMirror */}} + {{end}}{{/* end if .Repository.IsMirror */}} <table class="ui table"> <thead> <tr> - <th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}</th> + <th class="tw-w-2/5">{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}</th> <th>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}</th> <th>{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}</th> <th></th> @@ -230,13 +230,13 @@ > {{svg "octicon-pencil" 14}} </button> - <form method="post" class="gt-dib"> + <form method="post" class="tw-inline-block"> {{$.CsrfTokenHtml}} <input type="hidden" name="action" value="push-mirror-sync"> <input type="hidden" name="push_mirror_id" value="{{.ID}}"> <button class="ui primary tiny button" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.sync_mirror"}}">{{svg "octicon-sync" 14}}</button> </form> - <form method="post" class="gt-dib"> + <form method="post" class="tw-inline-block"> {{$.CsrfTokenHtml}} <input type="hidden" name="action" value="push-mirror-remove"> <input type="hidden" name="push_mirror_id" value="{{.ID}}"> @@ -262,10 +262,10 @@ <p class="help">{{ctx.Locale.Tr "repo.mirror_address_desc"}}</p> </div> <details class="ui optional field" {{if or .Err_PushMirrorAuth .push_mirror_username}}open{{end}}> - <summary class="gt-p-2"> + <summary class="tw-p-1"> {{ctx.Locale.Tr "repo.need_auth"}} </summary> - <div class="gt-p-2"> + <div class="tw-p-1"> <div class="inline field {{if .Err_PushMirrorAuth}}error{{end}}"> <label for="push_mirror_username">{{ctx.Locale.Tr "username"}}</label> <input id="push_mirror_username" name="push_mirror_username" value="{{.push_mirror_username}}"> @@ -335,13 +335,17 @@ <label>{{ctx.Locale.Tr "repo.settings.use_internal_wiki"}}</label> </div> </div> + <div class="inline field tw-pl-4"> + <label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label> + <input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}"> + </div> <div class="field"> <div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> <input class="enable-system-radio" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.UnitEnabled $.Context $.UnitTypeExternalWiki}}checked{{end}}> <label>{{ctx.Locale.Tr "repo.settings.use_external_wiki"}}</label> </div> </div> - <div class="field gt-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box"> + <div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}}disabled{{end}}" id="external_wiki_box"> <label for="external_wiki_url">{{ctx.Locale.Tr "repo.settings.external_wiki_url"}}</label> <input id="external_wiki_url" name="external_wiki_url" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}"> <p class="help">{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}</p> @@ -368,7 +372,7 @@ <label>{{ctx.Locale.Tr "repo.settings.use_internal_issue_tracker"}}</label> </div> </div> - <div class="field gt-pl-4 {{if (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box"> + <div class="field tw-pl-4 {{if (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box"> {{if .Repository.CanEnableTimetracker}} <div class="field"> <div class="ui checkbox"> @@ -400,7 +404,7 @@ <label>{{ctx.Locale.Tr "repo.settings.use_external_issue_tracker"}}</label> </div> </div> - <div class="field gt-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box"> + <div class="field tw-pl-4 {{if not (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}}disabled{{end}}" id="external_issue_box"> <div class="field"> <label for="external_tracker_url">{{ctx.Locale.Tr "repo.settings.external_tracker_url"}}</label> <input id="external_tracker_url" name="external_tracker_url" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerURL}}"> @@ -409,7 +413,7 @@ <div class="field"> <label for="tracker_url_format">{{ctx.Locale.Tr "repo.settings.tracker_url_format"}}</label> <input id="tracker_url_format" name="tracker_url_format" type="url" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerFormat}}" placeholder="https://github.com/{user}/{repo}/issues/{index}"> - <p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc" | Str2html}}</p> + <p class="help">{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc"}}</p> </div> <div class="inline fields"> <label for="issue_style">{{ctx.Locale.Tr "repo.settings.tracker_issue_style"}}</label> @@ -437,7 +441,7 @@ <div class="field {{if ne $externalTrackerStyle "regexp"}}disabled{{end}}" id="tracker-issue-style-regex-box"> <label for="external_tracker_regexp_pattern">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern"}}</label> <input id="external_tracker_regexp_pattern" name="external_tracker_regexp_pattern" value="{{(.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker).ExternalTrackerConfig.ExternalTrackerRegexpPattern}}"> - <p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}</p> + <p class="help">{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc"}}</p> </div> </div> </div> @@ -446,13 +450,45 @@ {{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}} {{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}} + {{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}} <div class="inline field"> <label>{{ctx.Locale.Tr "repo.project_board"}}</label> <div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> - <input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}> + <input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}> <label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label> </div> </div> + <div class="field {{if not $isProjectsEnabled}} disabled{{end}} tw-pl-4" id="projects_box"> + <p> + {{ctx.Locale.Tr "repo.settings.projects_mode_desc"}} + </p> + <div class="ui dropdown selection"> + <select name="projects_mode"> + <option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option> + <option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option> + <option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option> + </select> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="default text"> + {{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}} + {{ctx.Locale.Tr "repo.settings.projects_mode_repo"}} + {{end}} + {{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}} + {{ctx.Locale.Tr "repo.settings.projects_mode_owner"}} + {{end}} + {{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}} + {{ctx.Locale.Tr "repo.settings.projects_mode_all"}} + {{end}} + </div> + <div class="menu"> + <div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div> + <div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div> + <div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div> + </div> + </div> + </div> + + <div class="divider"></div> {{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}} {{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}} @@ -528,6 +564,12 @@ <label>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</label> </div> </div> + <div class="field"> + <div class="ui checkbox"> + <input name="pulls_allow_fast_forward_only" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowFastForwardOnly)}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</label> + </div> + </div> <div class="field"> <div class="ui checkbox"> <input name="pulls_allow_manual_merge" type="checkbox" {{if or (not $pullRequestEnabled) ($prUnit.PullRequestsConfig.AllowManualMerge)}}checked{{end}}> @@ -545,6 +587,7 @@ <option value="rebase" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</option> <option value="rebase-merge" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "rebase-merge")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</option> <option value="squash" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</option> + <option value="fast-forward-only" {{if or (not $pullRequestEnabled) (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}}selected{{end}}>{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</option> </select>{{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="default text"> {{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "merge")}} @@ -559,12 +602,16 @@ {{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "squash")}} {{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}} {{end}} + {{if (eq $prUnit.PullRequestsConfig.DefaultMergeStyle "fast-forward-only")}} + {{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}} + {{end}} </div> <div class="menu"> <div class="item" data-value="merge">{{ctx.Locale.Tr "repo.pulls.merge_pull_request"}}</div> <div class="item" data-value="rebase">{{ctx.Locale.Tr "repo.pulls.rebase_merge_pull_request"}}</div> <div class="item" data-value="rebase-merge">{{ctx.Locale.Tr "repo.pulls.rebase_merge_commit_pull_request"}}</div> <div class="item" data-value="squash">{{ctx.Locale.Tr "repo.pulls.squash_merge_pull_request"}}</div> + <div class="item" data-value="fast-forward-only">{{ctx.Locale.Tr "repo.pulls.fast_forward_only_merge_pull_request"}}</div> </div> </div> </div> @@ -790,7 +837,7 @@ </div> </div> {{if not .Repository.IsMirror}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-main"> {{if .Repository.IsArchived}} <div class="flex-item-title">{{ctx.Locale.Tr "repo.settings.unarchive.header"}}</div> @@ -922,8 +969,8 @@ </div> <div class="content"> <div class="ui warning message"> - {{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br> - {{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName | Safe}} + {{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br> + {{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}} {{if .Repository.NumForks}}<br> {{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}} {{end}} @@ -957,8 +1004,8 @@ </div> <div class="content"> <div class="ui warning message"> - {{ctx.Locale.Tr "repo.settings.delete_notices_1" | Safe}}<br> - {{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name | Safe}} + {{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br> + {{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}} </div> <form class="ui form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index 9c0fbddf06..fec4d7c8d4 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -2,7 +2,7 @@ <div class="repo-setting-content"> <form class="ui form" action="{{.Link}}" method="post"> <h4 class="ui top attached header"> - {{ctx.Locale.Tr "repo.settings.branch_protection" (.Rule.RuleName|Escape) | Str2html}} + {{ctx.Locale.Tr "repo.settings.branch_protection" .Rule.RuleName}} </h4> <div class="ui attached segment branch-protection"> <h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_patterns"}}</h5> @@ -10,17 +10,17 @@ <label>{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern"}}</label> <input name="rule_name" type="text" value="{{.Rule.RuleName}}"> <input name="rule_id" type="hidden" value="{{.Rule.ID}}"> - <p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc" | Safe}}</p> + <p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc"}}</p> </div> <div class="field"> <label>{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns"}}</label> <input name="protected_file_patterns" type="text" value="{{.Rule.ProtectedFilePatterns}}"> - <p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc" | Safe}}</p> + <p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_protected_file_patterns_desc"}}</p> </div> <div class="field"> <label>{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns"}}</label> <input name="unprotected_file_patterns" type="text" value="{{.Rule.UnprotectedFilePatterns}}"> - <p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc" | Safe}}</p> + <p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_unprotected_file_patterns_desc"}}</p> </div> {{.CsrfTokenHtml}} @@ -52,7 +52,7 @@ <label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_users"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="whitelist_users" value="{{.whitelist_users}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div> <div class="menu"> {{range .Users}} <div class="item" data-value="{{.ID}}"> @@ -67,7 +67,7 @@ <label>{{ctx.Locale.Tr "repo.settings.protect_whitelist_teams"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="whitelist_teams" value="{{.whitelist_teams}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div> <div class="menu"> {{range .Teams}} <div class="item" data-value="{{.ID}}"> @@ -98,7 +98,7 @@ <div class="field"> <label>{{ctx.Locale.Tr "repo.settings.protect_required_approvals"}}</label> <input name="required_approvals" type="number" value="{{.Rule.RequiredApprovals}}"> - <p class="help gt-ml-0">{{ctx.Locale.Tr "repo.settings.protect_required_approvals_desc"}}</p> + <p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_required_approvals_desc"}}</p> </div> <div class="grouped fields"> <div class="field"> @@ -113,7 +113,7 @@ <label>{{ctx.Locale.Tr "repo.settings.protect_approvals_whitelist_users"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="approvals_whitelist_users" value="{{.approvals_whitelist_users}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div> <div class="menu"> {{range .Users}} <div class="item" data-value="{{.ID}}"> @@ -128,7 +128,7 @@ <label>{{ctx.Locale.Tr "repo.settings.protect_approvals_whitelist_teams"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="approvals_whitelist_teams" value="{{.approvals_whitelist_teams}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div> <div class="menu"> {{range .Teams}} <div class="item" data-value="{{.ID}}"> @@ -179,7 +179,7 @@ <tr> <td> <span>{{.}}</span> - <span class="status-check-matched-mark gt-hidden" data-status-check="{{.}}">{{ctx.Locale.Tr "repo.settings.protect_status_check_matched"}}</span> + <span class="status-check-matched-mark tw-hidden" data-status-check="{{.}}">{{ctx.Locale.Tr "repo.settings.protect_status_check_matched"}}</span> </td> </tr> {{else}} @@ -210,7 +210,7 @@ <label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_users"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="merge_whitelist_users" value="{{.merge_whitelist_users}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div> <div class="menu"> {{range .Users}} <div class="item" data-value="{{.ID}}"> @@ -225,7 +225,7 @@ <label>{{ctx.Locale.Tr "repo.settings.protect_merge_whitelist_teams"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="merge_whitelist_teams" value="{{.merge_whitelist_teams}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div> <div class="menu"> {{range .Teams}} <div class="item" data-value="{{.ID}}"> diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl index ed7762acc5..c9efb7b67e 100644 --- a/templates/repo/settings/tags.tmpl +++ b/templates/repo/settings/tags.tmpl @@ -1,7 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit")}} <div class="repo-setting-content"> {{if .Repository.IsArchived}} - <div class="ui warning message gt-text-center"> + <div class="ui warning message tw-text-center"> {{ctx.Locale.Tr "repo.settings.archive.tagsettings_unavailable"}} </div> {{else}} @@ -21,14 +21,14 @@ <div class="ui input"> <input class="prompt" name="name_pattern" autocomplete="off" value="{{.name_pattern}}" placeholder="v*" autofocus required> </div> - <div class="help">{{ctx.Locale.Tr "repo.settings.tags.protection.pattern.description" | Safe}}</div> + <div class="help">{{ctx.Locale.Tr "repo.settings.tags.protection.pattern.description"}}</div> </div> </div> <div class="whitelist field"> <label>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.users"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="allowlist_users" value="{{.allowlist_users}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_users"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.user_kind"}}</div> <div class="menu"> {{range .Users}} <div class="item" data-value="{{.ID}}"> @@ -43,7 +43,7 @@ <label>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.teams"}}</label> <div class="ui multiple search selection dropdown"> <input type="hidden" name="allowlist_teams" value="{{.allowlist_teams}}"> - <div class="default text">{{ctx.Locale.Tr "repo.settings.protect_whitelist_search_teams"}}</div> + <div class="default text">{{ctx.Locale.Tr "search.team_kind"}}</div> <div class="menu"> {{range .Teams}} <div class="item" data-value="{{.ID}}"> @@ -106,7 +106,7 @@ </td> <td class="right aligned"> <a class="ui tiny primary button" href="{{$.RepoLink}}/settings/tags/{{.ID}}">{{ctx.Locale.Tr "edit"}}</a> - <form class="gt-dib" action="{{$.RepoLink}}/settings/tags/delete" method="post"> + <form class="tw-inline-block" action="{{$.RepoLink}}/settings/tags/delete" method="post"> {{$.CsrfTokenHtml}} <input type="hidden" name="id" value="{{.ID}}"> <button class="ui tiny red button">{{ctx.Locale.Tr "remove"}}</button> diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index ed6e670d60..36e75a7eb5 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -3,68 +3,23 @@ <div class="ui right"> <div class="ui jump dropdown"> <div class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</div> - <div class="menu"> - <a class="item" href="{{.BaseLinkNew}}/gitea/new"> - {{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/gogs/new"> - {{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/slack/new"> - {{template "shared/webhook/icon" (dict "HookType" "slack" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/discord/new"> - {{template "shared/webhook/icon" (dict "HookType" "discord" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/dingtalk/new"> - {{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/telegram/new"> - {{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/msteams/new"> - {{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/feishu/new"> - {{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/matrix/new"> - {{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/wechatwork/new"> - {{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}} - </a> - <a class="item" href="{{.BaseLinkNew}}/packagist/new"> - {{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" 20)}} - {{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}} - </a> - </div> + {{template "repo/settings/webhook/link_menu" .}} </div> </div> </h4> <div class="ui attached segment"> <div class="ui list"> <div class="item"> - {{.Description | Str2html}} + {{.Description}} </div> {{range .Webhooks}} <div class="item truncated-item-container"> - <span class="text {{if eq .LastStatus 1}}green{{else if eq .LastStatus 2}}red{{else}}grey{{end}} gt-mr-3">{{svg "octicon-dot-fill" 22}}</span> - <div class="text truncate gt-f1 gt-mr-3"> + <span class="text {{if eq .LastStatus 1}}green{{else if eq .LastStatus 2}}red{{else}}grey{{end}} tw-mr-2">{{svg "octicon-dot-fill" 22}}</span> + <div class="text truncate tw-flex-1 tw-mr-2"> <a title="{{.URL}}" href="{{$.BaseLink}}/{{.ID}}">{{.URL}}</a> </div> - <a class="muted gt-p-3" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a> - <a class="delete-button gt-p-3" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}}</a> + <a class="muted tw-p-2" href="{{$.BaseLink}}/{{.ID}}">{{svg "octicon-pencil"}}</a> + <a class="delete-button tw-p-2" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}}</a> </div> {{end}} </div> diff --git a/templates/repo/settings/webhook/dingtalk.tmpl b/templates/repo/settings/webhook/dingtalk.tmpl index 32ca0d0807..0ba99e98ee 100644 --- a/templates/repo/settings/webhook/dingtalk.tmpl +++ b/templates/repo/settings/webhook/dingtalk.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "dingtalk"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://dingtalk.com" (ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk")}}</p> <form class="ui form" action="{{.BaseLink}}/dingtalk/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_PayloadURL}}error{{end}}"> diff --git a/templates/repo/settings/webhook/discord.tmpl b/templates/repo/settings/webhook/discord.tmpl index 25dc219ee1..104346e042 100644 --- a/templates/repo/settings/webhook/discord.tmpl +++ b/templates/repo/settings/webhook/discord.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "discord"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://discord.com" (ctx.Locale.Tr "repo.settings.web_hook_name_discord")}}</p> <form class="ui form" action="{{.BaseLink}}/discord/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_PayloadURL}}error{{end}}"> diff --git a/templates/repo/settings/webhook/feishu.tmpl b/templates/repo/settings/webhook/feishu.tmpl index 9683427fbf..d80deab26f 100644 --- a/templates/repo/settings/webhook/feishu.tmpl +++ b/templates/repo/settings/webhook/feishu.tmpl @@ -1,6 +1,6 @@ {{if eq .HookType "feishu"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu") | Str2html}}</p> - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://feishu.cn" (ctx.Locale.Tr "repo.settings.web_hook_name_feishu")}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://larksuite.com" (ctx.Locale.Tr "repo.settings.web_hook_name_larksuite")}}</p> <form class="ui form" action="{{.BaseLink}}/feishu/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_PayloadURL}}error{{end}}"> diff --git a/templates/repo/settings/webhook/gitea.tmpl b/templates/repo/settings/webhook/gitea.tmpl index 4fda6a7b39..e6eb61ea92 100644 --- a/templates/repo/settings/webhook/gitea.tmpl +++ b/templates/repo/settings/webhook/gitea.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "gitea"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gitea")}}</p> <form class="ui form" action="{{.BaseLink}}/gitea/{{or .Webhook.ID "new"}}" method="post"> {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} diff --git a/templates/repo/settings/webhook/gogs.tmpl b/templates/repo/settings/webhook/gogs.tmpl index d2bd98c32c..e91a3279e4 100644 --- a/templates/repo/settings/webhook/gogs.tmpl +++ b/templates/repo/settings/webhook/gogs.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "gogs"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://docs.gitea.com/usage/webhooks" (ctx.Locale.Tr "repo.settings.web_hook_name_gogs")}}</p> <form class="ui form" action="{{.BaseLink}}/gogs/{{or .Webhook.ID "new"}}" method="post"> {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index 3c21a42421..8ee1446a16 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -6,7 +6,9 @@ <div class="ui right"> <!-- the button is wrapped with a span because the tooltip doesn't show on hover if we put data-tooltip-content directly on the button --> <span data-tooltip-content="{{if or $isNew .Webhook.IsActive}}{{ctx.Locale.Tr "repo.settings.webhook.test_delivery_desc"}}{{else}}{{ctx.Locale.Tr "repo.settings.webhook.test_delivery_desc_disabled"}}{{end}}"> - <button class="ui teal tiny button{{if not (or $isNew .Webhook.IsActive)}} disabled{{end}}" id="test-delivery" data-link="{{.Link}}/test" data-redirect="{{.Link}}">{{ctx.Locale.Tr "repo.settings.webhook.test_delivery"}}</button> + <button class="ui teal tiny button{{if not (or $isNew .Webhook.IsActive)}} disabled{{end}}" id="test-delivery" data-link="{{.Link}}/test" data-redirect="{{.Link}}"> + <span class="text">{{ctx.Locale.Tr "repo.settings.webhook.test_delivery"}}</span> + </button> </span> </div> {{end}} @@ -15,10 +17,12 @@ <div class="ui list"> {{range .History}} <div class="item"> - <div class="flex-text-block gt-sb"> + <div class="flex-text-block tw-justify-between"> <div class="flex-text-inline"> {{if .IsSucceed}} <span class="text green">{{svg "octicon-check"}}</span> + {{else if not .IsDelivered}} + <span class="text orange">{{svg "octicon-stopwatch"}}</span> {{else}} <span class="text red">{{svg "octicon-alert"}}</span> {{end}} @@ -28,7 +32,7 @@ {{TimeSince .Delivered.AsTime ctx.Locale}} </span> </div> - <div class="info gt-hidden" id="info-{{.ID}}"> + <div class="info tw-hidden" id="info-{{.ID}}"> <div class="ui top attached tabular menu"> <a class="item active" data-tab="request-{{.ID}}">{{ctx.Locale.Tr "repo.settings.webhook.request"}}</a> <a class="item" data-tab="response-{{.ID}}"> @@ -62,7 +66,7 @@ {{range $key, $val := .RequestInfo.Headers}}<strong>{{$key}}:</strong> {{$val}} {{end}}</pre> <h5>{{ctx.Locale.Tr "repo.settings.webhook.payload"}}</h5> - <pre class="webhook-info"><code class="json">{{.PayloadContent}}</code></pre> + <pre class="webhook-info"><code class="json">{{or .RequestInfo.Body .PayloadContent}}</code></pre> {{else}} - {{end}} diff --git a/templates/repo/settings/webhook/link_menu.tmpl b/templates/repo/settings/webhook/link_menu.tmpl new file mode 100644 index 0000000000..e2c86dcc3c --- /dev/null +++ b/templates/repo/settings/webhook/link_menu.tmpl @@ -0,0 +1,50 @@ +{{$size := 20}} +{{if .Size}} + {{$size = .Size}} +{{end}} +<div class="menu"> + <a class="item" href="{{.BaseLinkNew}}/gitea/new"> + {{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/gogs/new"> + {{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/slack/new"> + {{template "shared/webhook/icon" (dict "HookType" "slack" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/discord/new"> + {{template "shared/webhook/icon" (dict "HookType" "discord" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/dingtalk/new"> + {{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/telegram/new"> + {{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/msteams/new"> + {{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/feishu/new"> + {{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/matrix/new"> + {{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/wechatwork/new"> + {{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}} + </a> + <a class="item" href="{{.BaseLinkNew}}/packagist/new"> + {{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" $size)}} + {{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}} + </a> +</div> diff --git a/templates/repo/settings/webhook/matrix.tmpl b/templates/repo/settings/webhook/matrix.tmpl index a2a9921d7b..7f1c9f08e6 100644 --- a/templates/repo/settings/webhook/matrix.tmpl +++ b/templates/repo/settings/webhook/matrix.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "matrix"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://matrix.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_matrix")}}</p> <form class="ui form" action="{{.BaseLink}}/matrix/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_HomeserverURL}}error{{end}}"> diff --git a/templates/repo/settings/webhook/msteams.tmpl b/templates/repo/settings/webhook/msteams.tmpl index 0097209db1..62ea24e763 100644 --- a/templates/repo/settings/webhook/msteams.tmpl +++ b/templates/repo/settings/webhook/msteams.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "msteams"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://teams.microsoft.com" (ctx.Locale.Tr "repo.settings.web_hook_name_msteams")}}</p> <form class="ui form" action="{{.BaseLink}}/msteams/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_PayloadURL}}error{{end}}"> diff --git a/templates/repo/settings/webhook/packagist.tmpl b/templates/repo/settings/webhook/packagist.tmpl index fc373951d1..25aba2a435 100644 --- a/templates/repo/settings/webhook/packagist.tmpl +++ b/templates/repo/settings/webhook/packagist.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "packagist"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://packagist.org" (ctx.Locale.Tr "repo.settings.web_hook_name_packagist")}}</p> <form class="ui form" action="{{.BaseLink}}/packagist/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_Username}}error{{end}}"> diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index addf99d45a..6862ce5a2c 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -5,24 +5,24 @@ <div class="field"> <div class="ui radio non-events checkbox"> <input name="events" type="radio" value="push_only" {{if or $isNew .Webhook.PushOnly}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.settings.event_push_only" | Str2html}}</label> + <label>{{ctx.Locale.Tr "repo.settings.event_push_only"}}</label> </div> </div> <div class="field"> <div class="ui radio non-events checkbox"> <input name="events" type="radio" value="send_everything" {{if .Webhook.SendEverything}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.settings.event_send_everything" | Str2html}}</label> + <label>{{ctx.Locale.Tr "repo.settings.event_send_everything"}}</label> </div> </div> <div class="field"> <div class="ui radio events checkbox"> <input name="events" type="radio" value="choose_events" {{if .Webhook.ChooseEvents}}checked{{end}}> - <label>{{ctx.Locale.Tr "repo.settings.event_choose" | Str2html}}</label> + <label>{{ctx.Locale.Tr "repo.settings.event_choose"}}</label> </div> </div> </div> - <div class="events fields ui grid {{if not .Webhook.ChooseEvents}}gt-hidden{{end}}"> + <div class="events fields ui grid {{if not .Webhook.ChooseEvents}}tw-hidden{{end}}"> <!-- Repository Events --> <div class="fourteen wide column"> <label>{{ctx.Locale.Tr "repo.settings.event_header_repository"}}</label> @@ -254,8 +254,8 @@ <!-- Branch filter --> <div class="field"> <label for="branch_filter">{{ctx.Locale.Tr "repo.settings.branch_filter"}}</label> - <input name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}"> - <span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" | Str2html}}</span> + <input id="branch_filter" name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}"> + <span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc"}}</span> </div> <!-- Authorization Header --> @@ -263,7 +263,7 @@ <label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label> <input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}> {{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}} - <span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" "<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | Str2html}}</span> + <span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | SafeHTML)}}</span> {{end}} </div> diff --git a/templates/repo/settings/webhook/slack.tmpl b/templates/repo/settings/webhook/slack.tmpl index b367aed5ec..e7cae92d4b 100644 --- a/templates/repo/settings/webhook/slack.tmpl +++ b/templates/repo/settings/webhook/slack.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "slack"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://slack.com" (ctx.Locale.Tr "repo.settings.web_hook_name_slack")}}</p> <form class="ui form" action="{{.BaseLink}}/slack/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_PayloadURL}}error{{end}}"> diff --git a/templates/repo/settings/webhook/telegram.tmpl b/templates/repo/settings/webhook/telegram.tmpl index 92bbbef3fd..f92c2be0db 100644 --- a/templates/repo/settings/webhook/telegram.tmpl +++ b/templates/repo/settings/webhook/telegram.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "telegram"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://core.telegram.org/bots" (ctx.Locale.Tr "repo.settings.web_hook_name_telegram")}}</p> <form class="ui form" action="{{.BaseLink}}/telegram/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_BotToken}}error{{end}}"> diff --git a/templates/repo/settings/webhook/wechatwork.tmpl b/templates/repo/settings/webhook/wechatwork.tmpl index 65f12998b1..78a1617123 100644 --- a/templates/repo/settings/webhook/wechatwork.tmpl +++ b/templates/repo/settings/webhook/wechatwork.tmpl @@ -1,5 +1,5 @@ {{if eq .HookType "wechatwork"}} - <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork") | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://work.weixin.qq.com" (ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork")}}</p> <form class="ui form" action="{{.BaseLink}}/wechatwork/{{or .Webhook.ID "new"}}" method="post"> {{.CsrfTokenHtml}} <div class="required field {{if .Err_PayloadURL}}error{{end}}"> diff --git a/templates/repo/star_unstar.tmpl b/templates/repo/star_unstar.tmpl index 9c342f4065..1cdb98bf27 100644 --- a/templates/repo/star_unstar.tmpl +++ b/templates/repo/star_unstar.tmpl @@ -1,11 +1,10 @@ -<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star"> +<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}unstar{{else}}star{{end}}"> <div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.star_guest_user"}}"{{end}}> - <button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}}> - {{if $.IsStaringRepo}} - {{svg "octicon-star-fill"}}<span class="text">{{ctx.Locale.Tr "repo.unstar"}}</span> - {{else}} - {{svg "octicon-star"}}<span class="text">{{ctx.Locale.Tr "repo.star"}}</span> - {{end}} + {{$buttonText := ctx.Locale.Tr "repo.star"}} + {{if $.IsStaringRepo}}{{$buttonText = ctx.Locale.Tr "repo.unstar"}}{{end}} + <button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}} aria-label="{{$buttonText}}"> + {{if $.IsStaringRepo}}{{svg "octicon-star-fill"}}{{else}}{{svg "octicon-star"}}{{end}} + <span class="not-mobile" aria-hidden="true">{{$buttonText}}</span> </button> <a hx-boost="false" class="ui basic label" href="{{$.RepoLink}}/stars"> {{CountFmt .Repository.NumStars}} diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl index 8edb0c1516..000e0a10c5 100644 --- a/templates/repo/sub_menu.tmpl +++ b/templates/repo/sub_menu.tmpl @@ -1,5 +1,5 @@ {{if and (not .HideRepoInfo) (not .IsBlame)}} -<div class="ui segments repository-summary gt-mt-2 gt-mb-0"> +<div class="ui segments repository-summary tw-mt-1 tw-mb-0"> <div class="ui segment sub-menu repository-menu"> {{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}} <a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}"> @@ -21,11 +21,11 @@ {{end}} </div> {{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo) .LanguageStats}} - <div class="ui segment sub-menu language-stats-details gt-hidden"> + <div class="ui segment sub-menu language-stats-details tw-hidden"> {{range .LanguageStats}} <div class="item"> <i class="color-icon" style="background-color: {{.Color}}"></i> - <span class="gt-font-semibold"> + <span class="tw-font-semibold"> {{if eq .Language "other"}} {{ctx.Locale.Tr "repo.language_other"}} {{else}} diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl index 9f0676e395..5378a8a322 100644 --- a/templates/repo/tag/list.tmpl +++ b/templates/repo/tag/list.tmpl @@ -5,8 +5,8 @@ {{template "base/alert" .}} {{template "repo/release_tag_header" .}} <h4 class="ui top attached header"> - <div class="five wide column gt-df gt-ac"> - {{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.tags"}} + <div class="five wide column tw-flex tw-items-center"> + {{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.tags"}} </div> </h4> {{$canReadReleases := $.Permission.CanRead $.UnitTypeReleases}} @@ -16,38 +16,38 @@ {{range $idx, $release := .Releases}} <tr> <td class="tag"> - <h3 class="release-tag-name gt-mb-3"> + <h3 class="release-tag-name tw-mb-2"> {{if $canReadReleases}} - <a class="gt-df gt-ac" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> + <a class="tw-flex tw-items-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> {{else}} - <a class="gt-df gt-ac" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> + <a class="tw-flex tw-items-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> {{end}} </h3> - <div class="download gt-df gt-ac"> + <div class="download tw-flex tw-items-center"> {{if $.Permission.CanRead $.UnitTypeCode}} {{if .CreatedUnix}} - <span class="gt-mr-3">{{svg "octicon-clock" 16 "gt-mr-2"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span> + <span class="tw-mr-2">{{svg "octicon-clock" 16 "tw-mr-1"}}{{TimeSinceUnix .CreatedUnix ctx.Locale}}</span> {{end}} - <a class="gt-mr-3 gt-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "gt-mr-2"}}{{ShortSha .Sha1}}</a> + <a class="tw-mr-2 tw-font-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .Sha1}}</a> {{if not $.DisableDownloadSourceArchives}} - <a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}ZIP</a> - <a class="archive-link gt-mr-3 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "gt-mr-2"}}TAR.GZ</a> + <a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}ZIP</a> + <a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}TAR.GZ</a> {{end}} {{if (and $canReadReleases $.CanCreateRelease $release.IsTag)}} - <a class="gt-mr-3 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a> + <a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a> {{end}} {{if (and ($.Permission.CanWrite $.UnitTypeCode) $release.IsTag)}} - <a class="ui delete-button gt-mr-3 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}"> - {{svg "octicon-trash" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.delete_tag"}} + <a class="ui delete-button tw-mr-2 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}"> + {{svg "octicon-trash" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.delete_tag"}} </a> {{end}} {{if and $canReadReleases (not $release.IsTag)}} - <a class="gt-mr-3 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "gt-mr-2"}}{{ctx.Locale.Tr "repo.release.detail"}}</a> + <a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.detail"}}</a> {{end}} {{end}} </div> diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index d0730f23c1..8bceafa8bb 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,22 +1,22 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}} - <div class="ui warning message unicode-escape-prompt gt-text-left"> + <div class="ui warning message unicode-escape-prompt tw-text-left"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.invisible_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.invisible_runes_description" | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p> {{if .EscapeStatus.HasAmbiguous}} - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description" | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> {{end}} </div> {{else if .EscapeStatus.HasAmbiguous}} - <div class="ui warning message unicode-escape-prompt gt-text-left"> + <div class="ui warning message unicode-escape-prompt tw-text-left"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.ambiguous_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description" | Str2html}}</p> + <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> </div> {{end}} {{end}} diff --git a/templates/repo/user_cards.tmpl b/templates/repo/user_cards.tmpl index 12fb23f067..5accc2c7af 100644 --- a/templates/repo/user_cards.tmpl +++ b/templates/repo/user_cards.tmpl @@ -18,7 +18,7 @@ {{else if .Location}} {{svg "octicon-location"}} {{.Location}} {{else}} - {{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}} + {{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix)}} {{end}} </div> </li> diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 1f9e0b5028..0683004718 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -1,23 +1,23 @@ <div {{if .ReadmeInList}}id="readme" {{end}}class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content"> {{- if .FileError}} <div class="ui error message"> - <div class="text left gt-whitespace-pre">{{.FileError}}</div> + <div class="text left tw-whitespace-pre">{{.FileError}}</div> </div> {{end}} {{- if .FileWarning}} <div class="ui warning message"> - <div class="text left gt-whitespace-pre">{{.FileWarning}}</div> + <div class="text left tw-whitespace-pre">{{.FileWarning}}</div> </div> {{end}} {{if not .ReadmeInList}} - <div id="repo-file-commit-box" class="ui top attached header list-header gt-mb-4"> - <div> + <div id="repo-file-commit-box" class="ui top attached header list-header tw-mb-4 tw-flex tw-justify-between"> + <div class="latest-commit"> {{template "repo/latest_commit" .}} </div> {{if .LatestCommit}} {{if .LatestCommit.Committer}} - <div class="ui text grey right age"> + <div class="text grey age"> {{TimeSince .LatestCommit.Committer.When ctx.Locale}} </div> {{end}} @@ -25,24 +25,24 @@ </div> {{end}} - <h4 class="file-header ui top attached header gt-df gt-ac gt-sb gt-fw"> - <div class="file-header-left gt-df gt-ac gt-py-3 gt-pr-4"> + <h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap"> + <div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4"> {{if .ReadmeInList}} - {{svg "octicon-book" 16 "gt-mr-3"}} + {{svg "octicon-book" 16 "tw-mr-2"}} <strong><a class="default-link muted" href="#readme">{{.FileName}}</a></strong> {{else}} {{template "repo/file_info" .}} {{end}} </div> - <div class="file-header-right file-actions gt-df gt-ac gt-fw"> + <div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap"> {{if .HasSourceRenderedToggle}} <div class="ui compact icon buttons"> - <a href="{{$.Link}}?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a> + <a href="?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a> <a href="{{$.Link}}" class="ui mini basic button {{if .IsDisplayingRendered}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_rendered"}}">{{svg "octicon-file" 15}}</a> </div> {{end}} {{if not .ReadmeInList}} - <div class="ui buttons gt-mr-2"> + <div class="ui buttons tw-mr-1"> <a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a> {{if not .IsViewCommit}} <a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a> @@ -52,7 +52,7 @@ {{end}} <a class="ui mini basic button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_history"}}</a> {{if .EscapeStatus.Escaped}} - <button class="ui mini basic button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> + <button class="ui mini basic button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> <button class="ui mini basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> {{end}} </div> @@ -76,8 +76,8 @@ {{end}} {{end}} {{else if .EscapeStatus.Escaped}} - <button class="ui mini basic button unescape-button gt-mr-2 gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> - <button class="ui mini basic button escape-button gt-mr-2">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> + <button class="ui mini basic button unescape-button tw-mr-1 tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button> + <button class="ui mini basic button escape-button tw-mr-1">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> {{end}} {{if and .ReadmeInList .CanEditReadmeFile}} <a class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.editor.edit_this_file"}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}/{{PathEscapeSegments .FileName}}">{{svg "octicon-pencil"}}</a> @@ -89,7 +89,9 @@ {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} {{end}} <div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}"> - {{if .IsMarkup}} + {{if .IsFileTooLarge}} + {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} + {{else if .IsMarkup}} {{if .FileContent}}{{.FileContent}}{{end}} {{else if .IsPlainText}} <pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre> @@ -108,19 +110,10 @@ {{else if .IsPDFFile}} <div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div> {{else}} - <a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> + <a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> {{end}} </div> {{else if .FileSize}} - {{if .IsFileTooLarge}} - <table> - <tbody> - <tr> - <td><strong>{{ctx.Locale.Tr "repo.file_too_large"}}</strong></td> - </tr> - </tbody> - </table> - {{else}} <table> <tbody> {{range $idx, $code := .FileContent}} @@ -135,14 +128,13 @@ {{end}} </tbody> </table> - <div class="code-line-menu ui vertical pointing menu tippy-target"> + <div class="code-line-menu tippy-target"> {{if $.Permission.CanRead $.UnitTypeIssues}} - <a class="item ref-in-new-issue" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a> + <a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a> {{end}} - <a class="item view_git_blame" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a> - <a class="item copy-line-permalink" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a> + <a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a> + <a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a> </div> - {{end}} {{end}} </div> </div> diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index c1ef4ff4cb..fb257bd474 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -1,8 +1,12 @@ -<table id="repo-files-table" class="ui single line table gt-mt-0" data-last-commit-loader-url="{{.LastCommitLoaderURL}}"> +<table id="repo-files-table" class="ui single line table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}> <thead> <tr class="commit-list"> - <th colspan="2" {{if not .LatestCommit}}class="notready"{{end}}> - {{template "repo/latest_commit" .}} + <th class="tw-overflow-hidden" colspan="2"> + <div class="tw-flex"> + <div class="latest-commit"> + {{template "repo/latest_commit" .}} + </div> + </div> </th> <th class="text grey right age">{{if .LatestCommit}}{{if .LatestCommit.Committer}}{{TimeSince .LatestCommit.Committer.When ctx.Locale}}{{end}}{{end}}</th> </tr> @@ -55,7 +59,7 @@ {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} {{RenderCommitMessageLinkSubject $.Context $commit.Message $commitLink ($.Repository.ComposeMetas ctx)}} {{else}} - <div class="ui active tiny slow centered inline">…</div> + <div class="ui active tiny slow centered inline"></div> {{end}} </span> </td> diff --git a/templates/repo/watch_unwatch.tmpl b/templates/repo/watch_unwatch.tmpl index c42bc5a9e7..64be971416 100644 --- a/templates/repo/watch_unwatch.tmpl +++ b/templates/repo/watch_unwatch.tmpl @@ -1,11 +1,10 @@ -<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch"> +<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}"> <div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}> - <button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}}> - {{if $.IsWatchingRepo}} - {{svg "octicon-eye-closed" 16}}<span class="text">{{ctx.Locale.Tr "repo.unwatch"}}</span> - {{else}} - {{svg "octicon-eye"}}<span class="text">{{ctx.Locale.Tr "repo.watch"}}</span> - {{end}} + {{$buttonText := ctx.Locale.Tr "repo.watch"}} + {{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}} + <button type="submit" class="ui compact small basic button"{{if not $.IsSigned}} disabled{{end}} aria-label="{{$buttonText}}"> + {{svg "octicon-eye"}} + <span class="not-mobile" aria-hidden="true">{{$buttonText}}</span> </button> <a hx-boost="false" class="ui basic label" href="{{.RepoLink}}/watchers"> {{CountFmt .Repository.NumWatches}} diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl index ff31df0c32..0f10e60c4f 100644 --- a/templates/repo/wiki/new.tmpl +++ b/templates/repo/wiki/new.tmpl @@ -3,13 +3,13 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <div class="ui header flex-text-block gt-sb"> + <div class="ui header flex-text-block tw-justify-between"> {{ctx.Locale.Tr "repo.wiki.new_page"}} {{if .PageIsWikiEdit}} <a class="ui tiny primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a> {{end}} </div> - <form class="ui form" action="{{.Link}}?action={{if .PageIsWikiEdit}}_edit{{else}}_new{{end}}" method="post"> + <form class="ui form" action="?action={{if .PageIsWikiEdit}}_edit{{else}}_new{{end}}" method="post"> {{.CsrfTokenHtml}} <div class="field {{if .Err_Title}}error{{end}}"> <input name="title" value="{{.title}}" aria-label="{{ctx.Locale.Tr "repo.wiki.page_title"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.page_title"}}" autofocus required> @@ -31,14 +31,13 @@ "TextareaContent" $content )}} - <div class="field gt-mt-4"> + <div class="field tw-mt-4"> <input name="message" aria-label="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}" placeholder="{{ctx.Locale.Tr "repo.wiki.default_commit_message"}}"> </div> <div class="divider"></div> <div class="text right"> - <button class="ui primary button"> - {{ctx.Locale.Tr "repo.wiki.save_page"}} - </button> + <a class="ui basic cancel button" href="{{.Link}}">{{ctx.Locale.Tr "cancel"}}</a> + <button class="ui primary button">{{ctx.Locale.Tr "repo.wiki.save_page"}}</button> </div> </form> </div> diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl index a1bf13287c..52bf165e38 100644 --- a/templates/repo/wiki/pages.tmpl +++ b/templates/repo/wiki/pages.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository wiki pages"> {{template "repo/header" .}} <div class="ui container"> - <h2 class="ui header gt-df gt-ac gt-sb"> + <h2 class="ui header tw-flex tw-items-center tw-justify-between"> <span>{{ctx.Locale.Tr "repo.wiki.pages"}}</span> <span> {{if and .CanWriteWiki (not .Repository.IsMirror)}} @@ -10,6 +10,7 @@ {{end}} </span> </h2> + {{if .IsRepositoryAdmin}}<div>{{ctx.Locale.Tr "repo.default_branch"}}: {{.Repository.DefaultWikiBranch}}</div>{{end}} <table class="ui table wiki-pages-list"> <tbody> {{range .Pages}} @@ -20,7 +21,7 @@ <a class="wiki-git-entry" href="{{$.RepoLink}}/wiki/{{.GitEntryName | PathEscape}}" data-tooltip-content="{{ctx.Locale.Tr "repo.wiki.original_git_entry_tooltip"}}">{{svg "octicon-chevron-right"}}</a> </td> {{$timeSince := TimeSinceUnix .UpdatedUnix ctx.Locale}} - <td class="text right">{{ctx.Locale.Tr "repo.wiki.last_updated" $timeSince | Safe}}</td> + <td class="text right">{{ctx.Locale.Tr "repo.wiki.last_updated" $timeSince}}</td> </tr> {{end}} </tbody> diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl index 95b3cd0920..8e0060d4b3 100644 --- a/templates/repo/wiki/revision.tmpl +++ b/templates/repo/wiki/revision.tmpl @@ -10,19 +10,19 @@ {{$title}} <div class="ui sub header gt-word-break"> {{$timeSince := TimeSince .Author.When ctx.Locale}} - {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince | Safe}} + {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} </div> </div> </div> <div class="ui eight wide column text right"> - <div class="ui action small input" id="clone-panel"> + <div class="clone-panel ui action small input"> {{template "repo/clone_buttons" .}} {{template "repo/clone_script" .}} </div> </div> </div> <h2 class="ui top header">{{ctx.Locale.Tr "repo.wiki.wiki_page_revisions"}}</h2> - <div class="gt-mt-4"> + <div class="tw-mt-4"> <h4 class="ui top attached header"> <div class="ui stackable grid"> <div class="sixteen wide column"> diff --git a/templates/repo/wiki/start.tmpl b/templates/repo/wiki/start.tmpl index dca7a074aa..1b3c3d538a 100644 --- a/templates/repo/wiki/start.tmpl +++ b/templates/repo/wiki/start.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content repository wiki start"> {{template "repo/header" .}} <div class="ui container"> - <div class="ui center segment gt-py-5"> + <div class="ui center segment tw-py-8"> {{svg "octicon-book" 48}} <h2>{{ctx.Locale.Tr "repo.wiki.welcome"}}</h2> <p>{{ctx.Locale.Tr "repo.wiki.welcome_desc"}}</p> diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 039ff3f179..409de3ff08 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -4,8 +4,8 @@ {{$title := .title}} <div class="ui container"> <div class="repo-button-row"> - <div class="gt-df gt-ac"> - <div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "repo.pulls.no_results"}}"> + <div class="tw-flex tw-items-center"> + <div class="ui floating filter dropdown" data-no-results="{{ctx.Locale.Tr "no_results_found"}}"> <div class="ui basic small button"> <span class="text"> {{ctx.Locale.Tr "repo.wiki.page"}}: @@ -28,7 +28,7 @@ </div> </div> </div> - <div class="ui action small input gt-df gt-ac" id="clone-panel"> + <div class="clone-panel ui action small input"> {{template "repo/clone_buttons" .}} {{template "repo/clone_script" .}} </div> @@ -40,12 +40,12 @@ {{$title}} <div class="ui sub header"> {{$timeSince := TimeSince .Author.When ctx.Locale}} - {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince | Safe}} + {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} </div> </div> <div class="eight wide right aligned column"> {{if .EscapeStatus.Escaped}} - <a class="ui small button unescape-button gt-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a> + <a class="ui small button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a> <a class="ui small button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a> {{end}} {{if and .CanWriteWiki (not .Repository.IsMirror)}} @@ -67,34 +67,34 @@ <div class="wiki-content-parts"> {{if .sidebarTocContent}} <div class="markup wiki-content-sidebar wiki-content-toc"> - {{.sidebarTocContent | Safe}} + {{.sidebarTocContent | SafeHTML}} </div> {{end}} <div class="markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}"> {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} - {{.content | Safe}} + {{.content | SafeHTML}} </div> {{if .sidebarPresent}} <div class="markup wiki-content-sidebar"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} - <a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> + <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}} - {{.sidebarContent | Safe}} + {{.sidebarContent | SafeHTML}} </div> {{end}} - <div class="gt-clear-both"></div> + <div class="tw-clear-both"></div> {{if .footerPresent}} <div class="markup wiki-content-footer"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} - <a class="gt-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> + <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} {{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}} - {{.footerContent | Safe}} + {{.footerContent | SafeHTML}} </div> {{end}} </div> @@ -107,7 +107,7 @@ {{ctx.Locale.Tr "repo.wiki.delete_page_button"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" ($title|Escape) | Safe}}</p> + <p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" $title}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/shared/actions/runner_badge.tmpl b/templates/shared/actions/runner_badge.tmpl new file mode 100644 index 0000000000..816e87e177 --- /dev/null +++ b/templates/shared/actions/runner_badge.tmpl @@ -0,0 +1,25 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="18" + role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}"> + <title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title> + <linearGradient id="s" x2="0" y2="100%"> + <stop offset="0" stop-color="#fff" stop-opacity=".7" /> + <stop offset=".1" stop-color="#aaa" stop-opacity=".1" /> + <stop offset=".9" stop-color="#000" stop-opacity=".3" /> + <stop offset="1" stop-color="#000" stop-opacity=".5" /> + </linearGradient> + <clipPath id="r"> + <rect width="{{.Badge.Width}}" height="18" rx="4" fill="#fff" /> + </clipPath> + <g clip-path="url(#r)"> + <rect width="{{.Badge.Label.Width}}" height="18" fill="#555" /> + <rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="18" fill="{{.Badge.Color}}" /> + <rect width="{{.Badge.Width}}" height="18" fill="url(#s)" /> + </g> + <g fill="#fff" text-anchor="middle" font-family="Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" + font-size="{{.Badge.FontSize}}"><text aria-hidden="true" x="{{.Badge.Label.X}}" y="140" fill="#010101" fill-opacity=".3" + transform="scale(.1)" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text x="{{.Badge.Label.X}}" y="130" + transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text><text aria-hidden="true" + x="{{.Badge.Message.X}}" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" + textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text><text x="{{.Badge.Message.X}}" y="130" transform="scale(.1)" + fill="#fff" textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text></g> +</svg> diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl index c10901501d..d60f10b71f 100644 --- a/templates/shared/actions/runner_edit.tmpl +++ b/templates/shared/actions/runner_edit.tmpl @@ -7,15 +7,15 @@ {{template "base/disable_form_autofill"}} {{.CsrfTokenHtml}} <div class="runner-basic-info"> - <div class="field gt-dib gt-mr-4"> + <div class="field tw-inline-block tw-mr-4"> <label>{{ctx.Locale.Tr "actions.runners.status"}}</label> <span class="ui {{if .Runner.IsOnline}}green{{else}}basic{{end}} label">{{.Runner.StatusLocaleName ctx.Locale}}</span> </div> - <div class="field gt-dib gt-mr-4"> + <div class="field tw-inline-block tw-mr-4"> <label>{{ctx.Locale.Tr "actions.runners.last_online"}}</label> <span>{{if .Runner.LastOnline}}{{TimeSinceUnix .Runner.LastOnline ctx.Locale}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</span> </div> - <div class="field gt-dib gt-mr-4"> + <div class="field tw-inline-block tw-mr-4"> <label>{{ctx.Locale.Tr "actions.runners.labels"}}</label> <span> {{range .Runner.AgentLabels}} @@ -23,7 +23,7 @@ {{end}} </span> </div> - <div class="field gt-dib gt-mr-4"> + <div class="field tw-inline-block tw-mr-4"> <label>{{ctx.Locale.Tr "actions.runners.owner_type"}}</label> <span data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</span> </div> @@ -89,7 +89,7 @@ {{ctx.Locale.Tr "actions.runners.delete_runner_header"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice" | Safe}}</p> + <p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice"}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index 0e8f3cb874..8163007993 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -33,11 +33,7 @@ </h4> <div class="ui attached segment"> <form class="ui form ignore-dirty" id="user-list-search-form" action="{{$.Link}}"> - <!-- Search Text --> - <div class="ui fluid action input"> - {{template "shared/searchinput" dict "Value" .Keyword}} - <button class="ui primary button">{{ctx.Locale.Tr "explore.search"}}</button> - </div> + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.runner_kind")}} </form> </div> <div class="ui attached table segment"> @@ -68,7 +64,7 @@ {{range .Runners}} <tr> <td> - <span class="ui {{if .IsOnline}}green{{else}}basic{{end}} label">{{.StatusLocaleName ctx.Locale}}</span> + <span class="ui {{if .IsOnline}}green{{end}} label">{{.StatusLocaleName ctx.Locale}}</span> </td> <td>{{.ID}}</td> <td><p data-tooltip-content="{{.Description}}">{{.Name}}</p></td> diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index c6e86133cd..96fcf04cef 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -49,7 +49,7 @@ Template Attributes: </text-expander> <script> if (localStorage?.getItem('markdown-editor-monospace') === 'true') { - document.querySelector('.markdown-text-editor').classList.add('gt-mono'); + document.querySelector('.markdown-text-editor').classList.add('tw-font-mono'); } </script> </div> diff --git a/templates/shared/filetoolarge.tmpl b/templates/shared/filetoolarge.tmpl new file mode 100644 index 0000000000..8842fb1b91 --- /dev/null +++ b/templates/shared/filetoolarge.tmpl @@ -0,0 +1,4 @@ +<div class="tw-p-4"> + {{ctx.Locale.Tr "repo.file_too_large"}} + {{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}} +</div> diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl index 089e80bd8b..a62714e988 100644 --- a/templates/shared/issueicon.tmpl +++ b/templates/shared/issueicon.tmpl @@ -1,15 +1,15 @@ {{if .IsPull}} - {{if and .PullRequest .PullRequest.HasMerged}} - {{svg "octicon-git-merge" 16 "text purple"}} - {{else if and (.GetPullRequest ctx) (.GetPullRequest ctx).HasMerged}} - {{svg "octicon-git-merge" 16 "text purple"}} + {{if not .PullRequest}} + No PullRequest {{else}} {{if .IsClosed}} - {{svg "octicon-git-pull-request" 16 "text red"}} + {{if .PullRequest.HasMerged}} + {{svg "octicon-git-merge" 16 "text purple"}} + {{else}} + {{svg "octicon-git-pull-request" 16 "text red"}} + {{end}} {{else}} - {{if and .PullRequest (.PullRequest.IsWorkInProgress ctx)}} - {{svg "octicon-git-pull-request-draft" 16 "text grey"}} - {{else if and (.GetPullRequest ctx) ((.GetPullRequest ctx).IsWorkInProgress ctx)}} + {{if .PullRequest.IsWorkInProgress ctx}} {{svg "octicon-git-pull-request-draft" 16 "text grey"}} {{else}} {{svg "octicon-git-pull-request" 16 "text green"}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 8fe5aadf2b..1c0dfcc551 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -5,7 +5,7 @@ <div class="flex-item-icon"> {{if $.CanWriteIssuesOrPulls}} - <input type="checkbox" autocomplete="off" class="issue-checkbox gt-mr-4" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} "{{.Title}}""> + <input type="checkbox" autocomplete="off" class="issue-checkbox tw-mr-4" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} "{{.Title}}""> {{end}} {{template "shared/issueicon" .}} </div> @@ -13,15 +13,15 @@ <div class="flex-item-main"> <div class="flex-item-header"> <div class="flex-item-title"> - <a class="gt-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{RenderEmoji $.Context .Title | RenderCodeBlock}}</a> + <a class="tw-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{RenderEmoji $.Context .Title | RenderCodeBlock}}</a> {{if .IsPull}} {{if (index $.CommitStatuses .PullRequest.ID)}} {{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}} {{end}} {{end}} - <span class="labels-list gt-ml-2"> + <span class="labels-list tw-ml-1"> {{range .Labels}} - <a href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context .}}</a> + <a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{RenderLabel $.Context ctx.Locale .}}</a> {{end}} </span> </div> @@ -36,7 +36,7 @@ {{if .Assignees}} <div class="text grey"> {{range .Assignees}} - <a class="ui assignee gt-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}"> + <a class="ui assignee tw-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}"> {{ctx.AvatarUtils.Avatar . 20}} </a> {{end}} @@ -44,7 +44,7 @@ {{end}} {{if .NumComments}} <div class="text grey"> - <a class="gt-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> + <a class="tw-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> {{svg "octicon-comment" 16}}{{.NumComments}} </a> </div> @@ -62,11 +62,11 @@ </a> {{$timeStr := TimeSinceUnix .GetLastEventTimestamp ctx.Locale}} {{if .OriginalAuthor}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}} {{else if gt .Poster.ID 0}} - {{ctx.Locale.Tr .GetLastEventLabel $timeStr (.Poster.HomeLink|Escape) (.Poster.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabel $timeStr .Poster.HomeLink .Poster.GetDisplayName}} {{else}} - {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}} + {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}} {{end}} {{if .IsPull}} <div class="branches flex-text-inline"> @@ -153,7 +153,7 @@ {{end}} {{if .IssueIndexerUnavailable}} <div class="ui error message"> - <p>{{ctx.Locale.Tr "repo.issues.keyword_search_unavailable"}}</p> + <p>{{ctx.Locale.Tr "search.keyword_search_unavailable"}}</p> </div> {{end}} </div> diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo_search.tmpl new file mode 100644 index 0000000000..7fcb5d2361 --- /dev/null +++ b/templates/shared/repo_search.tmpl @@ -0,0 +1,64 @@ +<div class="ui small secondary filter menu"> + <form id="repo-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-gap-x-2"> + {{if .Language}}<input type="hidden" name="language" value="{{.Language}}">{{end}} + {{if .PageIsExploreRepositories}}<input type="hidden" name="only_show_relevant" value="{{.OnlyShowRelevant}}">{{end}} + {{if .TabName}}<input type="hidden" name="tab" value="{{.TabName}}">{{end}} + {{if .TopicOnly}}<input type="hidden" name="topic" value="{{.TopicOnly}}">{{end}} + <div class="ui small fluid action input tw-flex-1"> + {{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.repo_kind")}} + {{template "shared/search/button"}} + </div> + <!-- Filter --> + <div class="item ui small dropdown jump"> + <span class="text">{{ctx.Locale.Tr "filter"}}</span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu flex-items-menu"> + <label class="item"><input type="radio" name="clear-filter"> {{ctx.Locale.Tr "filter.clear"}}</label> + <div class="divider"></div> + <label class="item"><input type="radio" name="archived" {{if .IsArchived.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_archived"}}</label> + <label class="item"><input type="radio" name="archived" {{if (not (.IsArchived.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_archived"}}</label> + <div class="divider"></div> + <label class="item"><input type="radio" name="fork" {{if .IsFork.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_fork"}}</label> + <label class="item"><input type="radio" name="fork" {{if (not (.IsFork.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_fork"}}</label> + <div class="divider"></div> + <label class="item"><input type="radio" name="mirror" {{if .IsMirror.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_mirror"}}</label> + <label class="item"><input type="radio" name="mirror" {{if (not (.IsMirror.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_mirror"}}</label> + <div class="divider"></div> + <label class="item"><input type="radio" name="template" {{if .IsTemplate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.is_template"}}</label> + <label class="item"><input type="radio" name="template" {{if (not (.IsTemplate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.not_template"}}</label> + <div class="divider"></div> + <label class="item"><input type="radio" name="private" {{if .IsPrivate.Value}}checked{{end}} value="1"> {{ctx.Locale.Tr "filter.private"}}</label> + <label class="item"><input type="radio" name="private" {{if (not (.IsPrivate.ValueOrDefault true))}}checked{{end}} value="0"> {{ctx.Locale.Tr "filter.public"}}</label> + </div> + </div> + <!-- Sort --> + <div class="item ui small dropdown jump"> + <span class="text">{{ctx.Locale.Tr "repo.issues.filter_sort"}}</span> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="menu"> + <label class="{{if eq .SortType "newest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "newest"}}checked{{end}} value="newest"> {{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</label> + <label class="{{if eq .SortType "oldest"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "oldest"}}checked{{end}} value="oldest"> {{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</label> + <label class="{{if eq .SortType "alphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "alphabetically"}}checked{{end}} value="alphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</label> + <label class="{{if eq .SortType "reversealphabetically"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversealphabetically"}}checked{{end}} value="reversealphabetically"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</label> + <label class="{{if eq .SortType "recentupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "recentupdate"}}checked{{end}} value="recentupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</label> + <label class="{{if eq .SortType "leastupdate"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "leastupdate"}}checked{{end}} value="leastupdate"> {{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</label> + {{if not .DisableStars}} + <label class="{{if eq .SortType "moststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "moststars"}}checked{{end}} value="moststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</label> + <label class="{{if eq .SortType "feweststars"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "feweststars"}}checked{{end}} value="feweststars"> {{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</label> + {{end}} + <label class="{{if eq .SortType "mostforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "mostforks"}}checked{{end}} value="mostforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</label> + <label class="{{if eq .SortType "fewestforks"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "fewestforks"}}checked{{end}} value="fewestforks"> {{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</label> + <label class="{{if eq .SortType "size"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "size"}}checked{{end}} value="size"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.by_size"}}</label> + <label class="{{if eq .SortType "reversesize"}}active {{end}}item"><input hidden type="radio" name="sort" {{if eq .SortType "reversesize"}}checked{{end}} value="reversesize"> {{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_by_size"}}</label> + </div> + </div> + </form> +</div> +{{if and .PageIsExploreRepositories .OnlyShowRelevant}} + <div class="ui message"> + <span data-tooltip-content="{{ctx.Locale.Tr "explore.relevant_repositories_tooltip"}}"> + {{ctx.Locale.Tr "explore.relevant_repositories" (printf "?only_show_relevant=0&sort=%s&q=%s&language=%s" $.SortType (QueryEscape $.Keyword) (QueryEscape $.Language))}} + </span> + </div> +{{end}} +<div class="divider"></div> diff --git a/templates/shared/search/button.tmpl b/templates/shared/search/button.tmpl new file mode 100644 index 0000000000..7bb1662e15 --- /dev/null +++ b/templates/shared/search/button.tmpl @@ -0,0 +1,3 @@ +{{/* Disable (optional) - if search button has to be disabled */}} +{{/* Tooltip (optional) - a tooltip to be displayed on hover */}} +<button class="ui small icon button" aria-label="{{ctx.Locale.Tr "search.search"}}" {{with .Tooltip}}data-tooltip-content="{{.}}"{{end}}{{if .Disabled}} disabled{{end}}>{{svg "octicon-search"}}</button> diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl new file mode 100644 index 0000000000..a98a662654 --- /dev/null +++ b/templates/shared/search/code/results.tmpl @@ -0,0 +1,36 @@ +<div class="flex-text-block tw-flex-wrap"> + {{range $term := .SearchResultLanguages}} + <a class="ui {{if eq $.Language $term.Language}}primary{{end}} basic label tw-m-0" + href="?q={{$.Keyword}}{{if ne $.Language $term.Language}}&l={{$term.Language}}{{end}}&fuzzy={{$.IsFuzzy}}"> + <i class="color-icon tw-mr-2" style="background-color: {{$term.Color}}"></i> + {{$term.Language}} + <div class="detail">{{$term.Count}}</div> + </a> + {{end}} +</div> +<div class="repository search"> + {{range $result := .SearchResults}} + {{$repo := or $.Repo (index $.RepoMaps .RepoID)}} + <div class="diff-file-box diff-box file-content non-diff-file-content repo-search-result"> + <h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap"> + {{if not $.Repo}} + <span class="file tw-flex-1"> + <a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a> + {{if $repo.IsArchived}} + <span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span> + {{end}} + - {{.Filename}} + </span> + {{else}} + <span class="file tw-flex-1">{{.Filename}}</span> + {{end}} + <a role="button" class="ui basic tiny button" rel="nofollow" href="{{$repo.Link}}/src/commit/{{$result.CommitID | PathEscape}}/{{.Filename | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> + </h4> + <div class="ui attached table segment"> + {{template "shared/searchfile" dict "RepoLink" $repo.Link "SearchResult" .}} + </div> + {{template "shared/searchbottom" dict "root" $ "result" .}} + </div> + {{end}} +</div> +{{template "base/paginate" .}} diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl new file mode 100644 index 0000000000..cb873f5a92 --- /dev/null +++ b/templates/shared/search/code/search.tmpl @@ -0,0 +1,22 @@ +<form class="ui form ignore-dirty"> + {{template "shared/search/combo_fuzzy" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind")}} +</form> +<div class="divider"></div> +<div class="ui user list"> + {{if .CodeIndexerUnavailable}} + <div class="ui error message"> + <p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p> + </div> + {{else}} + {{if not .CodeIndexerEnabled}} + <div class="ui message"> + <p>{{ctx.Locale.Tr "search.code_search_by_git_grep"}}</p> + </div> + {{end}} + {{if .SearchResults}} + {{template "shared/search/code/results" .}} + {{else if .Keyword}} + <div>{{ctx.Locale.Tr "search.no_results"}}</div> + {{end}} + {{end}} +</div> diff --git a/templates/shared/search/combo.tmpl b/templates/shared/search/combo.tmpl new file mode 100644 index 0000000000..788db95cc1 --- /dev/null +++ b/templates/shared/search/combo.tmpl @@ -0,0 +1,8 @@ +{{/* Value - value of the search field (for search results page) */}} +{{/* Disabled (optional) - if search field/button has to be disabled */}} +{{/* Placeholder (optional) - placeholder text to be used */}} +{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}} +<div class="ui small fluid action input"> + {{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}} + {{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}} +</div> diff --git a/templates/shared/search/combo_fuzzy.tmpl b/templates/shared/search/combo_fuzzy.tmpl new file mode 100644 index 0000000000..3540a89ecb --- /dev/null +++ b/templates/shared/search/combo_fuzzy.tmpl @@ -0,0 +1,10 @@ +{{/* Value - value of the search field (for search results page) */}} +{{/* Disabled (optional) - if search field/button has to be disabled */}} +{{/* Placeholder (optional) - placeholder text to be used */}} +{{/* IsFuzzy - state of the fuzzy search toggle */}} +{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}} +<div class="ui small fluid action input"> + {{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}} + {{template "shared/search/fuzzy" dict "Disabled" .Disabled "IsFuzzy" .IsFuzzy}} + {{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}} +</div> diff --git a/templates/shared/search/fuzzy.tmpl b/templates/shared/search/fuzzy.tmpl new file mode 100644 index 0000000000..6ddb03c004 --- /dev/null +++ b/templates/shared/search/fuzzy.tmpl @@ -0,0 +1,10 @@ +{{/* Disabled (optional) - if dropdown has to be disabled */}} +{{/* IsFuzzy - state of the fuzzy search toggle */}} +<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}"> + <input name="fuzzy" type="hidden"{{if .Disabled}} disabled{{end}} value="{{.IsFuzzy}}">{{svg "octicon-triangle-down" 14 "dropdown icon"}} + <div class="text">{{if .IsFuzzy}}{{ctx.Locale.Tr "search.fuzzy"}}{{else}}{{ctx.Locale.Tr "search.match"}}{{end}}</div> + <div class="menu"> + <div class="item" data-value="true" data-tooltip-content="{{ctx.Locale.Tr "search.fuzzy_tooltip"}}">{{ctx.Locale.Tr "search.fuzzy"}}</div> + <div class="item" data-value="false" data-tooltip-content="{{ctx.Locale.Tr "search.match_tooltip"}}">{{ctx.Locale.Tr "search.match"}}</div> + </div> +</div> diff --git a/templates/shared/search/input.tmpl b/templates/shared/search/input.tmpl new file mode 100644 index 0000000000..75bed07b80 --- /dev/null +++ b/templates/shared/search/input.tmpl @@ -0,0 +1,4 @@ +{{/* Value - value of the search field (for search results page) */}} +{{/* Disabled (optional) - if search field has to be disabled */}} +{{/* Placeholder (optional) - placeholder text to be used */}} +<input type="search" name="q"{{with .Value}} value="{{.}}"{{end}} maxlength="255" spellcheck="false" placeholder="{{with .Placeholder}}{{.}}{{else}}{{ctx.Locale.Tr "search.search"}}{{end}}"{{if .Disabled}} disabled{{end}}> diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl index 55b6cb2909..bee0397259 100644 --- a/templates/shared/searchbottom.tmpl +++ b/templates/shared/searchbottom.tmpl @@ -1,12 +1,14 @@ -<div class="ui bottom attached table segment gt-df gt-ac gt-sb"> - <div class="gt-df gt-ac gt-ml-4"> +{{if or .result.Language (not .result.UpdatedUnix.IsZero)}} +<div class="ui bottom attached table segment tw-flex tw-items-center tw-justify-between"> + <div class="tw-flex tw-items-center tw-ml-4"> {{if .result.Language}} - <i class="color-icon gt-mr-3" style="background-color: {{.result.Color}}"></i>{{.result.Language}} + <i class="color-icon tw-mr-2" style="background-color: {{.result.Color}}"></i>{{.result.Language}} {{end}} </div> - <div class="gt-mr-4"> + <div class="tw-mr-4"> {{if not .result.UpdatedUnix.IsZero}} - <span class="ui grey text">{{ctx.Locale.Tr "explore.code_last_indexed_at" (TimeSinceUnix .result.UpdatedUnix ctx.Locale) | Safe}}</span> + <span class="ui grey text">{{ctx.Locale.Tr "explore.code_last_indexed_at" (TimeSinceUnix .result.UpdatedUnix ctx.Locale)}}</span> {{end}} </div> </div> +{{end}} diff --git a/templates/shared/searchfile.tmpl b/templates/shared/searchfile.tmpl new file mode 100644 index 0000000000..280584e4d1 --- /dev/null +++ b/templates/shared/searchfile.tmpl @@ -0,0 +1,14 @@ +<div class="file-body file-code code-view"> + <table> + <tbody> + {{range .SearchResult.Lines}} + <tr> + <td class="lines-num"> + <a href="{{$.RepoLink}}/src/commit/{{PathEscape $.SearchResult.CommitID}}/{{PathEscapeSegments $.SearchResult.Filename}}#L{{.Num}}"><span>{{.Num}}</span></a> + </td> + <td class="lines-code chroma"><code class="code-inner">{{.FormattedContent}}</code></td> + </tr> + {{end}} + </tbody> + </table> +</div> diff --git a/templates/shared/searchinput.tmpl b/templates/shared/searchinput.tmpl deleted file mode 100644 index 48b288c299..0000000000 --- a/templates/shared/searchinput.tmpl +++ /dev/null @@ -1 +0,0 @@ -<input type="search" spellcheck="false" name="q" maxlength="255" placeholder="{{ctx.Locale.Tr "explore.search"}}…"{{if .Value}} value="{{.Value}}"{{end}}{{if .Disabled}} disabled{{end}}> diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl index 7192f31fb2..ea59459083 100644 --- a/templates/shared/secrets/add_list.tmpl +++ b/templates/shared/secrets/add_list.tmpl @@ -14,7 +14,7 @@ {{if .Secrets}} <div class="flex-list"> {{range .Secrets}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> {{svg "octicon-key" 32}} </div> @@ -28,9 +28,9 @@ </div> <div class="flex-item-trailing"> <span class="color-text-light-2"> - {{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} + {{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} </span> - <button class="ui btn interact-bg link-action gt-p-3" + <button class="ui btn interact-bg link-action tw-p-2" data-url="{{$.Link}}/delete?id={{.ID}}" data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}" data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}" diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl index 64ccc62cd0..d57a635b4b 100644 --- a/templates/shared/user/authorlink.tmpl +++ b/templates/shared/user/authorlink.tmpl @@ -1 +1 @@ -<a class="author text black gt-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label gt-p-2">bot</span>{{end}} +<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1">bot</span>{{end}} diff --git a/templates/shared/user/block_user_dialog.tmpl b/templates/shared/user/block_user_dialog.tmpl new file mode 100644 index 0000000000..c6db4ca1e4 --- /dev/null +++ b/templates/shared/user/block_user_dialog.tmpl @@ -0,0 +1,23 @@ +<div class="ui small modal" id="block-user-modal"> + <div class="header">{{ctx.Locale.Tr "user.block.title"}}</div> + <div class="content"> + <div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}}</div> + <form class="ui form modal-form" method="post"> + {{.CsrfTokenHtml}} + <input type="hidden" name="action" value="block" /> + <input type="hidden" name="blockee" class="modal-blockee" /> + <div class="field"> + <label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="text red modal-blockee-name"></span></label> + </div> + <div class="field"> + <label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label> + <input id="block-note" name="note"> + <p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p> + </div> + <div class="text right actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button> + <button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button> + </div> + </form> + </div> +</div> diff --git a/templates/shared/user/blocked_users.tmpl b/templates/shared/user/blocked_users.tmpl new file mode 100644 index 0000000000..e83a039ef5 --- /dev/null +++ b/templates/shared/user/blocked_users.tmpl @@ -0,0 +1,83 @@ +<h4 class="ui top attached header"> + {{ctx.Locale.Tr "user.block.title"}} +</h4> +<div class="ui attached segment"> + <p>{{ctx.Locale.Tr "user.block.info_1"}}</p> + <ul> + <li>{{ctx.Locale.Tr "user.block.info_2"}}</li> + <li>{{ctx.Locale.Tr "user.block.info_3"}}</li> + <li>{{ctx.Locale.Tr "user.block.info_4"}}</li> + <li>{{ctx.Locale.Tr "user.block.info_5"}}</li> + <li>{{ctx.Locale.Tr "user.block.info_6"}}</li> + <li>{{ctx.Locale.Tr "user.block.info_7"}}</li> + </ul> +</div> +<div class="ui segment"> + <form class="ui form ignore-dirty" action="{{$.Link}}" method="post"> + {{.CsrfTokenHtml}} + <input type="hidden" name="action" value="block" /> + <div id="search-user-box" class="field ui fluid search input"> + <input class="prompt tw-mr-2" name="blockee" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required> + <button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button> + </div> + <div class="field"> + <label>{{ctx.Locale.Tr "user.block.note.title"}}</label> + <input name="note"> + <p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p> + </div> + </form> +</div> +<h4 class="ui top attached header"> + {{ctx.Locale.Tr "user.block.list"}} +</h4> +<div class="ui attached segment"> + <div class="flex-list"> + {{range .UserBlocks}} + <div class="flex-item"> + <div class="flex-item-leading"> + {{ctx.AvatarUtils.Avatar .Blockee}} + </div> + <div class="flex-item-main"> + <div class="flex-item-title"> + <a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a> + </div> + {{if .Note}} + <div class="flex-item-body"> + <i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}} + </div> + {{end}} + </div> + <div class="flex-item-trailing"> + <button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button> + <form action="{{$.Link}}" method="post"> + {{$.CsrfTokenHtml}} + <input type="hidden" name="action" value="unblock" /> + <input type="hidden" name="blockee" value="{{.Blockee.Name}}" /> + <button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button> + </form> + </div> + </div> + {{else}} + <div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div> + {{end}} + </div> +</div> +<div class="ui small modal" id="block-user-note-modal"> + <div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div> + <div class="content"> + <form class="ui form" action="{{$.Link}}" method="post"> + {{.CsrfTokenHtml}} + <input type="hidden" name="action" value="note" /> + <input type="hidden" name="blockee" class="modal-blockee" /> + <div class="field"> + <label>{{ctx.Locale.Tr "user.block.note.title"}}</label> + <input name="note" class="modal-note" /> + <p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p> + </div> + <div class="text right actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button> + <button class="ui primary button">{{ctx.Locale.Tr "save"}}</button> + </div> + </form> + </div> +</div> diff --git a/templates/shared/user/org_profile_avatar.tmpl b/templates/shared/user/org_profile_avatar.tmpl index a8846b0abd..2ff1e40ca8 100644 --- a/templates/shared/user/org_profile_avatar.tmpl +++ b/templates/shared/user/org_profile_avatar.tmpl @@ -2,7 +2,7 @@ <div class="ui container"> <div class="ui vertically grid head"> <div class="column"> - <div class="ui header gt-df gt-ac gt-word-break"> + <div class="ui header tw-flex tw-items-center gt-word-break"> {{ctx.AvatarUtils.Avatar . 100}} <span class="text thin grey"><a href="{{.HomeLink}}">{{.DisplayName}}</a></span> <span class="org-visibility"> diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 4fbc43f541..868f8d5a13 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -1,5 +1,5 @@ <div id="profile-avatar-card" class="ui card"> - <div id="profile-avatar" class="content gt-df"> + <div id="profile-avatar" class="content tw-flex"> {{if eq .SignedUserID .ContextUser.ID}} <a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{ctx.Locale.Tr "user.change_avatar"}}"> {{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}} @@ -18,22 +18,27 @@ {{svg "octicon-gear" 18}} </a> {{end}}</span> - <div class="gt-mt-3"> - <a class="muted" href="{{.ContextUser.HomeLink}}?tab=followers">{{svg "octicon-person" 18 "gt-mr-2"}}{{.NumFollowers}} {{ctx.Locale.Tr "user.followers"}}</a> · <a class="muted" href="{{.ContextUser.HomeLink}}?tab=following">{{.NumFollowing}} {{ctx.Locale.Tr "user.following"}}</a> + <div class="tw-mt-2"> + <a class="muted" href="{{.ContextUser.HomeLink}}?tab=followers">{{svg "octicon-person" 18 "tw-mr-1"}}{{.NumFollowers}} {{ctx.Locale.Tr "user.followers"}}</a> · <a class="muted" href="{{.ContextUser.HomeLink}}?tab=following">{{.NumFollowing}} {{ctx.Locale.Tr "user.following"}}</a> {{if .EnableFeed}} - <a href="{{.ContextUser.HomeLink}}.rss"><i class="ui text grey gt-ml-3" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">{{svg "octicon-rss" 18}}</i></a> + <a href="{{.ContextUser.HomeLink}}.rss"><i class="ui text grey tw-ml-2" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">{{svg "octicon-rss" 18}}</i></a> {{end}} </div> </div> <div class="extra content gt-word-break"> <ul> + {{if .UserBlocking}} + <li class="text red">{{svg "octicon-circle-slash"}} {{ctx.Locale.Tr "user.block.blocked"}}</li> + {{if .UserBlocking.Note}} + <li class="text small red">{{ctx.Locale.Tr "user.block.note"}}: {{.UserBlocking.Note}}</li> + {{end}} + {{end}} {{if .ContextUser.Location}} <li> {{svg "octicon-location"}} - <span class="gt-f1">{{.ContextUser.Location}}</span> - {{if .UserLocationMapURL}} - {{/* We presume that the UserLocationMapURL is safe, as it is provided by the site administrator. */}} - <a href="{{.UserLocationMapURL | Safe}}{{.ContextUser.Location | QueryEscape}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}"> + <span class="tw-flex-1">{{.ContextUser.Location}}</span> + {{if .ContextUserLocationMapURL}} + <a href="{{.ContextUserLocationMapURL}}" rel="nofollow noreferrer" data-tooltip-content="{{ctx.Locale.Tr "user.show_on_map"}}"> {{svg "octicon-link-external"}} </a> {{end}} @@ -42,7 +47,7 @@ {{if (eq .SignedUserID .ContextUser.ID)}} <li> {{svg "octicon-mail"}} - <a class="gt-f1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a> + <a class="tw-flex-1" href="mailto:{{.ContextUser.Email}}" rel="nofollow">{{.ContextUser.Email}}</a> <a href="{{AppSubUrl}}/user/settings#privacy-user-settings"> {{if .ShowUserEmail}} <i data-tooltip-content="{{ctx.Locale.Tr "user.email_visibility.limited"}}"> @@ -71,7 +76,7 @@ {{end}} {{if $.RenderedDescription}} <li> - <div class="render-content markup">{{$.RenderedDescription|Str2html}}</div> + <div class="render-content markup">{{$.RenderedDescription}}</div> </li> {{end}} {{range .OpenIDs}} @@ -82,7 +87,7 @@ </li> {{end}} {{end}} - <li>{{svg "octicon-calendar"}} <span>{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .ContextUser.CreatedUnix) | Safe}}</span></li> + <li>{{svg "octicon-calendar"}} <span>{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .ContextUser.CreatedUnix)}}</span></li> {{if and .Orgs .HasOrgsVisible}} <li> <ul class="user-orgs"> @@ -110,18 +115,29 @@ </li> {{end}} {{if and .IsSigned (ne .SignedUserID .ContextUser.ID)}} - <li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card" > - {{if $.IsFollowing}} - <button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button"> - {{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}} - </button> - {{else}} - <button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button"> - {{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}} - </button> + {{if not .UserBlocking}} + <li class="follow" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card"> + {{if $.IsFollowing}} + <button hx-post="{{.ContextUser.HomeLink}}?action=unfollow" class="ui basic red button"> + {{svg "octicon-person"}} {{ctx.Locale.Tr "user.unfollow"}} + </button> + {{else}} + <button hx-post="{{.ContextUser.HomeLink}}?action=follow" class="ui basic primary button"> + {{svg "octicon-person"}} {{ctx.Locale.Tr "user.follow"}} + </button> + {{end}} + </li> {{end}} - </li> + <li> + {{if not .UserBlocking}} + <a class="muted show-modal" href="#" data-modal="#block-user-modal" data-modal-modal-blockee="{{.ContextUser.Name}}" data-modal-modal-blockee-name="{{.ContextUser.GetDisplayName}}" data-modal-modal-form.action="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.block.user"}}</a> + {{else}} + <a class="muted" href="{{AppSubUrl}}/user/settings/blocked_users">{{ctx.Locale.Tr "user.block.unblock"}}</a> + {{end}} + </li> {{end}} </ul> </div> </div> + +{{template "shared/user/block_user_dialog" .}} diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl index fc5cd966fc..06c71c0610 100644 --- a/templates/shared/variables/variable_list.tmpl +++ b/templates/shared/variables/variable_list.tmpl @@ -16,7 +16,7 @@ {{if .Variables}} <div class="flex-list"> {{range .Variables}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> {{svg "octicon-pencil" 32}} </div> @@ -30,9 +30,9 @@ </div> <div class="flex-item-trailing"> <span class="color-text-light-2"> - {{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} + {{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} </span> - <button class="btn interact-bg gt-p-3 show-modal" + <button class="btn interact-bg tw-p-2 show-modal" data-tooltip-content="{{ctx.Locale.Tr "actions.variables.edit"}}" data-modal="#edit-variable-modal" data-modal-form.action="{{$.Link}}/{{.ID}}/edit" @@ -42,7 +42,7 @@ > {{svg "octicon-pencil"}} </button> - <button class="btn interact-bg gt-p-3 link-action" + <button class="btn interact-bg tw-p-2 link-action" data-tooltip-content="{{ctx.Locale.Tr "actions.variables.deletion"}}" data-url="{{$.Link}}/{{.ID}}/delete" data-modal-confirm="{{ctx.Locale.Tr "actions.variables.deletion.description"}}" diff --git a/templates/shared/webhook/icon.tmpl b/templates/shared/webhook/icon.tmpl index 84f9de266f..0f80787c57 100644 --- a/templates/shared/webhook/icon.tmpl +++ b/templates/shared/webhook/icon.tmpl @@ -3,7 +3,7 @@ {{$size = .Size}} {{end}} {{if eq .HookType "gitea"}} - <img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gitea.svg"> + {{svg "gitea-gitea" $size "img"}} {{else if eq .HookType "gogs"}} <img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gogs.ico"> {{else if eq .HookType "slack"}} diff --git a/templates/status/404.tmpl b/templates/status/404.tmpl index 74bb8762bd..78f149e67b 100644 --- a/templates/status/404.tmpl +++ b/templates/status/404.tmpl @@ -1,14 +1,10 @@ {{template "base/head" .}} -<div role="main" aria-label="{{.Title}}" class="page-content ui container center gt-w-screen {{if .IsRepo}}repository{{end}}"> +<div role="main" aria-label="{{.Title}}" class="page-content {{if .IsRepo}}repository{{end}}"> {{if .IsRepo}}{{template "repo/header" .}}{{end}} - <div class="ui container center"> - <p style="margin-top: 100px"><img src="{{AssetUrlPrefix}}/img/404.png" alt="404"></p> - <p>{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404" | Safe}}{{end}}</p> - {{if .NotFoundGoBackURL}}<a class="ui button green" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>{{end}} - - <div class="divider"></div> - <br> - {{if .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}} + <div class="ui container tw-text-center"> + <img class="tw-max-w-[80vw] tw-py-16" src="{{AssetUrlPrefix}}/img/404.png" alt="404"> + <p>{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404"}}{{end}}</p> + {{if .NotFoundGoBackURL}}<a class="ui button" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>{{end}} </div> </div> {{template "base/footer" .}} diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index edcb90f9a4..576b6eebbb 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,5 +1,5 @@ {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics. -* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName, Str2html +* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl, ThemeName * ctx.Locale * .Flash * .ErrorMsg @@ -16,9 +16,9 @@ </head> <body> <div class="full height"> - <nav class="ui secondary menu gt-border-secondary-bottom"> - <div class="ui container gt-df"> - <div class="item gt-f1"> + <nav class="ui secondary menu"> + <div class="ui container tw-flex"> + <div class="item tw-flex-1"> <a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home"}}"> <img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true"> </a> @@ -28,21 +28,22 @@ </div> </div> </nav> + <div class="divider tw-my-0"></div> <div role="main" class="page-content status-page-500"> <div class="ui container" > <style> .ui.message.flash-message { text-align: left; } </style> {{template "base/alert" .}} </div> - <p class="gt-mt-5 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p> + <p class="tw-mt-8 center"><img src="{{AssetUrlPrefix}}/img/500.png" alt="Internal Server Error"></p> <div class="divider"></div> - <div class="ui container gt-my-5"> + <div class="ui container tw-my-8"> {{if .ErrorMsg}} <p>{{ctx.Locale.Tr "error.occurred"}}:</p> - <pre class="gt-whitespace-pre-wrap gt-break-all">{{.ErrorMsg}}</pre> + <pre class="tw-whitespace-pre-wrap tw-break-all">{{.ErrorMsg}}</pre> {{end}} - <div class="center gt-mt-5"> + <div class="center tw-mt-8"> {{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}} - {{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message" | Str2html}}</p>{{end}} + {{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message"}}</p>{{end}} </div> </div> </div> diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 403f241d72..b5677c77e0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -8,8 +8,8 @@ "text/html" ], "schemes": [ - "http", - "https" + "https", + "http" ], "swagger": "2.0", "info": { @@ -19,9 +19,9 @@ "name": "MIT", "url": "http://opensource.org/licenses/MIT" }, - "version": "{{AppVer | JSEscape | Safe}}" + "version": "{{AppVer | JSEscape}}" }, - "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1", + "basePath": "{{AppSubUrl | JSEscape}}/api/v1", "paths": { "/activitypub/user-id/{user-id}": { "get": { @@ -689,6 +689,109 @@ } } }, + "/admin/users/{username}/badges": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List a user's badges", + "operationId": "adminListUserBadges", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/BadgeList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add a badge to a user", + "operationId": "adminAddUserBadges", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UserBadgeOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Remove a badge from a user", + "operationId": "adminDeleteUserBadges", + "parameters": [ + { + "type": "string", + "description": "username of user", + "name": "username", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UserBadgeOption" + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/admin/users/{username}/keys": { "post": { "consumes": [ @@ -1741,6 +1844,232 @@ } } }, + "/orgs/{org}/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get an org-level variables list", + "operationId": "getOrgVariablesList", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/orgs/{org}/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get an org-level variable", + "operationId": "getOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Update an org-level variable", + "operationId": "updateOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create an org-level variable", + "operationId": "createOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Delete an org-level variable", + "operationId": "deleteOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + } + }, "/orgs/{org}/activities/feeds": { "get": { "produces": [ @@ -1852,6 +2181,151 @@ } } }, + "/orgs/{org}/blocks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List users blocked by the organization", + "operationId": "organizationListBlocks", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/UserList" + } + } + } + }, + "/orgs/{org}/blocks/{username}": { + "get": { + "tags": [ + "organization" + ], + "summary": "Check if a user is blocked by the organization", + "operationId": "organizationCheckUserBlock", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user to check", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "tags": [ + "organization" + ], + "summary": "Block a user", + "operationId": "organizationBlockUser", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user to block", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "optional note for the block", + "name": "note", + "in": "query" + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + }, + "delete": { + "tags": [ + "organization" + ], + "summary": "Unblock a user", + "operationId": "organizationUnblockUser", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user to unblock", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/orgs/{org}/hooks": { "get": { "produces": [ @@ -3475,6 +3949,261 @@ } } }, + "/repos/{owner}/{repo}/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get repo-level variables list", + "operationId": "getRepoVariablesList", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a repo-level variable", + "operationId": "getRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Update a repo-level variable", + "operationId": "updateRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a repo-level variable", + "operationId": "createRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete a repo-level variable", + "operationId": "deleteRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + } + }, "/repos/{owner}/{repo}/activities/feeds": { "get": { "produces": [ @@ -4237,6 +4966,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, @@ -4565,6 +5297,49 @@ } } }, + "/repos/{owner}/{repo}/commits/{sha}/pull": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get the pull request of the commit", + "operationId": "repoGetCommitPullRequest", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "SHA of the commit to get", + "name": "sha", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/PullRequest" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/contents": { "get": { "produces": [ @@ -6134,7 +6909,7 @@ }, { "type": "string", - "description": "Only show items which were created by the the given user", + "description": "Only show items which were created by the given user", "name": "created_by", "in": "query" }, @@ -6546,6 +7321,9 @@ "400": { "$ref": "#/responses/error" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/error" }, @@ -10315,6 +11093,9 @@ "201": { "$ref": "#/responses/PullRequest" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, @@ -10366,6 +11147,56 @@ } } }, + "/repos/{owner}/{repo}/pulls/{base}/{head}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a pull request by base and head", + "operationId": "repoGetPullRequestByBaseHead", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "base of the pull request to get", + "name": "base", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "head of the pull request to get", + "name": "head", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/PullRequest" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/pulls/{index}": { "get": { "produces": [ @@ -10926,6 +11757,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" }, @@ -12147,7 +12981,8 @@ }, "post": { "consumes": [ - "multipart/form-data" + "multipart/form-data", + "application/octet-stream" ], "produces": [ "application/json" @@ -12190,8 +13025,7 @@ "type": "file", "description": "attachment to upload", "name": "attachment", - "in": "formData", - "required": true + "in": "formData" } ], "responses": { @@ -12763,6 +13597,9 @@ "200": { "$ref": "#/responses/WatchInfo" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -14317,6 +15154,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -14691,6 +15531,194 @@ } } }, + "/user/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get the user-level list of variables which is created by current doer", + "operationId": "getUserVariablesList", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/user/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get a user-level variable which is created by current doer", + "operationId": "getUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update a user-level variable which is created by current doer", + "operationId": "updateUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Create a user-level variable", + "operationId": "createUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete a user-level variable which is created by current doer", + "operationId": "deleteUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "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" + } + } + } + }, "/user/applications/oauth2": { "get": { "produces": [ @@ -14885,6 +15913,123 @@ } } }, + "/user/blocks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "List users blocked by the authenticated user", + "operationId": "userListBlocks", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/UserList" + } + } + } + }, + "/user/blocks/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Check if a user is blocked by the authenticated user", + "operationId": "userCheckUserBlock", + "parameters": [ + { + "type": "string", + "description": "user to check", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Block a user", + "operationId": "userBlockUser", + "parameters": [ + { + "type": "string", + "description": "user to block", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "optional note for the block", + "name": "note", + "in": "query" + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Unblock a user", + "operationId": "userUnblockUser", + "parameters": [ + { + "type": "string", + "description": "user to unblock", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/user/emails": { "get": { "produces": [ @@ -15062,6 +16207,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -15769,6 +16917,9 @@ "204": { "$ref": "#/responses/empty" }, + "403": { + "$ref": "#/responses/forbidden" + }, "404": { "$ref": "#/responses/notFound" } @@ -16711,6 +17862,35 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActionVariable": { + "description": "ActionVariable return value of the query API", + "type": "object", + "properties": { + "data": { + "description": "the value of the variable", + "type": "string", + "x-go-name": "Data" + }, + "name": { + "description": "the name of the variable", + "type": "string", + "x-go-name": "Name" + }, + "owner_id": { + "description": "the owner to which the variable belongs", + "type": "integer", + "format": "int64", + "x-go-name": "OwnerID" + }, + "repo_id": { + "description": "the repository to which the variable belongs", + "type": "integer", + "format": "int64", + "x-go-name": "RepoID" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Activity": { "type": "object", "properties": { @@ -16910,6 +18090,30 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Badge": { + "description": "Badge represents a user badge", + "type": "object", + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "image_url": { + "type": "string", + "x-go-name": "ImageURL" + }, + "slug": { + "type": "string", + "x-go-name": "Slug" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Branch": { "description": "Branch represents a repository branch", "type": "object", @@ -18573,6 +19777,21 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateVariableOption": { + "description": "CreateVariableOption the option when creating variable", + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "description": "Value of the variable to create", + "type": "string", + "x-go-name": "Value" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreateWikiPageOptions": { "description": "CreateWikiPageOptions form for creating wiki", "type": "object", @@ -19195,6 +20414,11 @@ "description": "EditRepoOption options when editing a repository's properties", "type": "object", "properties": { + "allow_fast_forward_only_merge": { + "description": "either `true` to allow fast-forward-only merging pull requests, or `false` to prevent fast-forward-only merging.", + "type": "boolean", + "x-go-name": "AllowFastForwardOnly" + }, "allow_manual_merge": { "description": "either `true` to allow mark pr as merged manually, or `false` to prevent it.", "type": "boolean", @@ -19251,7 +20475,7 @@ "x-go-name": "DefaultDeleteBranchAfterMerge" }, "default_merge_style": { - "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", or \"squash\".", + "description": "set to a merge style to be used by this repository: \"merge\", \"rebase\", \"rebase-merge\", \"squash\", or \"fast-forward-only\".", "type": "string", "x-go-name": "DefaultMergeStyle" }, @@ -19330,6 +20554,11 @@ "type": "boolean", "x-go-name": "Private" }, + "projects_mode": { + "description": "`repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.", + "type": "string", + "x-go-name": "ProjectsMode" + }, "template": { "description": "either `true` to make this repository a template or `false` to make it a normal repository", "type": "boolean", @@ -20383,6 +21612,13 @@ "type": "object", "additionalProperties": {}, "x-go-name": "Validations" + }, + "visible": { + "type": "array", + "items": { + "$ref": "#/definitions/IssueFormFieldVisible" + }, + "x-go-name": "Visible" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -20392,6 +21628,11 @@ "title": "IssueFormFieldType defines issue form field type, can be \"markdown\", \"textarea\", \"input\", \"dropdown\" or \"checkboxes\"", "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "IssueFormFieldVisible": { + "description": "IssueFormFieldVisible defines issue form field visible", + "type": "string", + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "IssueLabelsOption": { "description": "IssueLabelsOption a collection of labels", "type": "object", @@ -20650,6 +21891,7 @@ "rebase", "rebase-merge", "squash", + "fast-forward-only", "manually-merged" ] }, @@ -22036,6 +23278,10 @@ "description": "Repository represents a repository", "type": "object", "properties": { + "allow_fast_forward_only_merge": { + "type": "boolean", + "x-go-name": "AllowFastForwardOnly" + }, "allow_merge_commits": { "type": "boolean", "x-go-name": "AllowMerge" @@ -22234,6 +23480,10 @@ "type": "boolean", "x-go-name": "Private" }, + "projects_mode": { + "type": "string", + "x-go-name": "ProjectsMode" + }, "release_counter": { "type": "integer", "format": "int64", @@ -22834,6 +24084,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateVariableOption": { + "description": "UpdateVariableOption the option when updating variable", + "type": "object", + "required": [ + "value" + ], + "properties": { + "name": { + "description": "New name for the variable. If the field is empty, the variable name won't be updated.", + "type": "string", + "x-go-name": "Name" + }, + "value": { + "description": "Value of the variable to update", + "type": "string", + "x-go-name": "Value" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "User": { "description": "User represents a user", "type": "object", @@ -22944,6 +24214,24 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UserBadgeOption": { + "description": "UserBadgeOption options for link between users and badges", + "type": "object", + "properties": { + "badge_slugs": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "BadgeSlugs", + "example": [ + "badge1", + "badge2" + ] + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UserHeatmapData": { "description": "UserHeatmapData represents the data needed to create a heatmap", "type": "object", @@ -23197,6 +24485,12 @@ } } }, + "ActionVariable": { + "description": "ActionVariable", + "schema": { + "$ref": "#/definitions/ActionVariable" + } + }, "ActivityFeedsList": { "description": "ActivityFeedsList", "schema": { @@ -23233,6 +24527,15 @@ } } }, + "BadgeList": { + "description": "BadgeList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Badge" + } + } + }, "Branch": { "description": "Branch", "schema": { @@ -24071,6 +25374,15 @@ } } }, + "VariableList": { + "description": "VariableList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionVariable" + } + } + }, "WatchInfo": { "description": "WatchInfo", "schema": { @@ -24146,7 +25458,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/CreateOrUpdateSecretOption" + "$ref": "#/definitions/UpdateVariableOption" } }, "redirect": { diff --git a/templates/user/auth/activate.tmpl b/templates/user/auth/activate.tmpl index 1b06719753..7f8ff0eb5a 100644 --- a/templates/user/auth/activate.tmpl +++ b/templates/user/auth/activate.tmpl @@ -2,47 +2,35 @@ <div role="main" aria-label="{{.Title}}" class="page-content user activate"> <div class="ui middle very relaxed page grid"> <div class="column"> - <form class="ui form ignore-dirty" action="{{AppSubUrl}}/user/activate" method="post"> + <form class="ui form ignore-dirty tw-max-w-2xl tw-m-auto" action="{{AppSubUrl}}/user/activate" method="post"> {{.CsrfTokenHtml}} <h2 class="ui top attached header"> {{ctx.Locale.Tr "auth.active_your_account"}} </h2> <div class="ui attached segment"> {{template "base/alert" .}} - {{if .IsActivatePage}} - {{if .ServiceNotEnabled}} - <p class="center">{{ctx.Locale.Tr "auth.disable_register_mail"}}</p> - {{else if .ResendLimited}} - <p class="center">{{ctx.Locale.Tr "auth.resent_limit_prompt"}}</p> - {{else}} - <p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.SignedUser.Email|Escape) .ActiveCodeLives | Str2html}}</p> - {{end}} + {{if .NeedVerifyLocalPassword}} + <div class="required field"> + <label for="verify-password">{{ctx.Locale.Tr "password"}}</label> + <input id="verify-password" name="password" type="password" autocomplete="off" required> + </div> + <div class="inline field"> + <button class="ui primary button">{{ctx.Locale.Tr "install.confirm_password"}}</button> + </div> + <input name="code" type="hidden" value="{{.ActivationCode}}"> {{else}} - {{if .NeedsPassword}} - <div class="required inline field"> - <label for="password">{{ctx.Locale.Tr "password"}}</label> - <input id="password" name="password" type="password" autocomplete="off" required> + <p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" .SignedUser.Name .SignedUser.Email}}</p> + <details> + <summary>{{ctx.Locale.Tr "auth.change_unconfirmed_mail_address"}}</summary> + <div class="tw-py-2"> + <label for="change-email">{{ctx.Locale.Tr "email"}}</label> + <input id="change-email" name="change_email" type="email" value="{{.SignedUser.Email}}"> </div> - <div class="inline field"> - <label></label> - <button class="ui primary button">{{ctx.Locale.Tr "install.confirm_password"}}</button> - </div> - <input id="code" name="code" type="hidden" value="{{.Code}}"> - {{else if .IsSendRegisterMail}} - <p>{{ctx.Locale.Tr "auth.confirmation_mail_sent_prompt" (.Email|Escape) .ActiveCodeLives | Str2html}}</p> - {{else if .IsCodeInvalid}} - <p>{{ctx.Locale.Tr "auth.invalid_code"}}</p> - {{else if .IsPasswordInvalid}} - <p>{{ctx.Locale.Tr "auth.invalid_password"}}</p> - {{else if .ManualActivationOnly}} - <p class="center">{{ctx.Locale.Tr "auth.manual_activation_only"}}</p> - {{else}} - <p>{{ctx.Locale.Tr "auth.has_unconfirmed_mail" (.SignedUser.Name|Escape) (.SignedUser.Email|Escape) | Str2html}}</p> - <div class="divider"></div> - <div class="text right"> - <button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button> - </div> - {{end}} + </details> + <div class="divider"></div> + <div class="text"> + <button class="ui primary button">{{ctx.Locale.Tr "auth.resend_mail"}}</button> + </div> {{end}} </div> </form> diff --git a/templates/user/auth/activate_prompt.tmpl b/templates/user/auth/activate_prompt.tmpl new file mode 100644 index 0000000000..237244df8c --- /dev/null +++ b/templates/user/auth/activate_prompt.tmpl @@ -0,0 +1,15 @@ +{{template "base/head" .}} +<div role="main" aria-label="{{.Title}}" class="page-content user activate"> + <div class="ui middle very relaxed page grid"> + <div class="column"> + <h2 class="ui top attached header"> + {{ctx.Locale.Tr "auth.active_your_account"}} + </h2> + <div class="ui attached segment"> + {{template "base/alert" .}} + <p>{{.ActivationPromptMessage}}</p> + </div> + </div> + </div> +</div> +{{template "base/footer" .}} diff --git a/templates/user/auth/captcha.tmpl b/templates/user/auth/captcha.tmpl index 1c3379e629..0e9c2b9d22 100644 --- a/templates/user/auth/captcha.tmpl +++ b/templates/user/auth/captcha.tmpl @@ -1,9 +1,8 @@ {{if .EnableCaptcha}}{{if eq .CaptchaType "image"}} <div class="inline field"> - <label>{{/* This is CAPTCHA field */}}</label> {{.Captcha.CreateHTML}} </div> - <div class="required inline field {{if .Err_Captcha}}error{{end}}"> + <div class="required field {{if .Err_Captcha}}error{{end}}"> <label for="captcha">{{ctx.Locale.Tr "captcha"}}</label> <input id="captcha" name="captcha" value="{{.captcha}}" autocomplete="off"> </div> @@ -24,7 +23,7 @@ <div id="captcha" data-captcha-type="m-captcha" data-sitekey="{{.McaptchaSitekey}}" data-instance-url="{{.McaptchaURL}}"></div> </div> {{else if eq .CaptchaType "cfturnstile"}} - <div class="inline field gt-text-center"> + <div class="inline field tw-text-center"> <div id="captcha" data-captcha-type="cf-turnstile" data-sitekey="{{.CfTurnstileSitekey}}"></div> </div> <script src='https://challenges.cloudflare.com/turnstile/v0/api.js'></script> diff --git a/templates/user/auth/change_passwd_inner.tmpl b/templates/user/auth/change_passwd_inner.tmpl index cffc798a64..01bbf500e5 100644 --- a/templates/user/auth/change_passwd_inner.tmpl +++ b/templates/user/auth/change_passwd_inner.tmpl @@ -5,18 +5,17 @@ {{ctx.Locale.Tr "settings.change_password"}} </h4> <div class="ui attached segment"> - <form class="ui form" action="{{.ChangePasscodeLink}}" method="post"> + <form class="ui form tw-max-w-2xl tw-m-auto" action="{{.ChangePasscodeLink}}" method="post"> {{.CsrfTokenHtml}} - <div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> + <div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> <label for="password">{{ctx.Locale.Tr "password"}}</label> <input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required> </div> - <div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> + <div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> <label for="retype">{{ctx.Locale.Tr "re_type"}}</label> <input id="retype" name="retype" type="password" autocomplete="new-password" required> </div> <div class="inline field"> - <label></label> <button class="ui primary button">{{ctx.Locale.Tr "settings.change_password"}}</button> </div> </form> diff --git a/templates/user/auth/finalize_openid.tmpl b/templates/user/auth/finalize_openid.tmpl index 7449e3beda..1c1dcdb825 100644 --- a/templates/user/auth/finalize_openid.tmpl +++ b/templates/user/auth/finalize_openid.tmpl @@ -35,7 +35,7 @@ {{if .ShowRegistrationButton}} <div class="inline field"> <label></label> - <a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a> + <a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a> </div> {{end}} </form> diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl index dde4c8f6fe..55bcf63018 100644 --- a/templates/user/auth/forgot_passwd.tmpl +++ b/templates/user/auth/forgot_passwd.tmpl @@ -10,15 +10,14 @@ <div class="ui attached segment"> {{template "base/alert" .}} {{if .IsResetSent}} - <p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" (Escape .Email) .ResetPwdCodeLives | Str2html}}</p> + <p>{{ctx.Locale.Tr "auth.reset_password_mail_sent_prompt" .Email .ResetPwdCodeLives}}</p> {{else if .IsResetRequest}} - <div class="required inline field {{if .Err_Email}}error{{end}}"> + <div class="required field {{if .Err_Email}}error{{end}}"> <label for="email">{{ctx.Locale.Tr "email"}}</label> <input id="email" name="email" type="email" value="{{.Email}}" autofocus required> </div> <div class="divider"></div> <div class="inline field"> - <label></label> <button class="ui primary button">{{ctx.Locale.Tr "auth.send_reset_mail"}}</button> </div> {{else if .IsResetDisable}} diff --git a/templates/user/auth/grant.tmpl b/templates/user/auth/grant.tmpl index 9c0bf33e28..cb9bba8749 100644 --- a/templates/user/auth/grant.tmpl +++ b/templates/user/auth/grant.tmpl @@ -9,11 +9,11 @@ {{template "base/alert" .}} <p> <b>{{ctx.Locale.Tr "auth.authorize_application_description"}}</b><br> - {{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML | Str2html}} + {{ctx.Locale.Tr "auth.authorize_application_created_by" .ApplicationCreatorLinkHTML}} </p> </div> <div class="ui attached segment"> - <p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML | Str2html}}</p> + <p>{{ctx.Locale.Tr "auth.authorize_redirect_notice" .ApplicationRedirectDomainHTML}}</p> </div> <div class="ui attached segment"> <form method="post" action="{{AppSubUrl}}/login/oauth/grant"> diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl index 5235cbf69f..8dd49ccd60 100644 --- a/templates/user/auth/link_account.tmpl +++ b/templates/user/auth/link_account.tmpl @@ -1,7 +1,7 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content user link-account"> - <div class="ui secondary pointing tabular top attached borderless menu new-menu navbar"> - <div class="new-menu-inner"> + <overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav"> + <div class="overflow-menu-items tw-justify-center"> <!-- TODO handle .ShowRegistrationButton once other login bugs are fixed --> {{if not .AllowOnlyInternalRegistration}} <a class="item {{if not .user_exists}}active{{end}}" @@ -14,7 +14,7 @@ {{ctx.Locale.Tr "auth.oauth_signin_tab"}} </a> </div> - </div> + </overflow-menu> <div class="ui middle very relaxed page grid"> <div class="column"> <div class="ui tab {{if not .user_exists}}active{{end}}" diff --git a/templates/user/auth/oidc_wellknown.tmpl b/templates/user/auth/oidc_wellknown.tmpl index 38e6900c38..54bb4a763d 100644 --- a/templates/user/auth/oidc_wellknown.tmpl +++ b/templates/user/auth/oidc_wellknown.tmpl @@ -1,16 +1,16 @@ { - "issuer": "{{AppUrl | JSEscape | Safe}}", - "authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize", - "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token", - "jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys", - "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo", - "introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect", + "issuer": "{{AppUrl | JSEscape}}", + "authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize", + "token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token", + "jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys", + "userinfo_endpoint": "{{AppUrl | JSEscape}}login/oauth/userinfo", + "introspection_endpoint": "{{AppUrl | JSEscape}}login/oauth/introspect", "response_types_supported": [ "code", "id_token" ], "id_token_signing_alg_values_supported": [ - "{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}" + "{{.SigningKey.SigningMethod.Alg | JSEscape}}" ], "subject_types_supported": [ "public" diff --git a/templates/user/auth/prohibit_login.tmpl b/templates/user/auth/prohibit_login.tmpl index 668aa20e71..962ddfa98c 100644 --- a/templates/user/auth/prohibit_login.tmpl +++ b/templates/user/auth/prohibit_login.tmpl @@ -2,7 +2,7 @@ <div role="main" aria-label="{{.Title}}" class="page-content user activate"> <div class="ui middle very relaxed page grid"> <div class="column"> - <form class="ui form"> + <form class="ui form tw-max-w-2xl tw-m-auto"> <h2 class="ui top attached header"> {{ctx.Locale.Tr "auth.prohibit_login"}} </h2> diff --git a/templates/user/auth/reset_passwd.tmpl b/templates/user/auth/reset_passwd.tmpl index 2f470df441..f8303feef3 100644 --- a/templates/user/auth/reset_passwd.tmpl +++ b/templates/user/auth/reset_passwd.tmpl @@ -17,13 +17,12 @@ </div> {{end}} {{if .IsResetForm}} - <div class="required inline field {{if .Err_Password}}error{{end}}"> + <div class="required field {{if .Err_Password}}error{{end}}"> <label for="password">{{ctx.Locale.Tr "settings.new_password"}}</label> <input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" autofocus required> </div> {{if not .user_signed_in}} <div class="inline field"> - <label></label> <div class="ui checkbox"> <label>{{ctx.Locale.Tr "auth.remember_me"}}</label> <input name="remember" type="checkbox"> @@ -34,7 +33,7 @@ <h4 class="ui dividing header"> {{ctx.Locale.Tr "twofa"}} </h4> - <div class="ui warning visible message">{{ctx.Locale.Tr "settings.twofa_is_enrolled" | Str2html}}</div> + <div class="ui warning visible message">{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}</div> {{if .scratch_code}} <div class="required inline field {{if .Err_Token}}error{{end}}"> <label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label> @@ -42,7 +41,7 @@ </div> <input type="hidden" name="scratch_code" value="true"> {{else}} - <div class="required inline field {{if .Err_Passcode}}error{{end}}"> + <div class="required field {{if .Err_Passcode}}error{{end}}"> <label for="passcode">{{ctx.Locale.Tr "passcode"}}</label> <input id="passcode" name="passcode" type="number" autocomplete="off" autofocus required> </div> @@ -50,14 +49,13 @@ {{end}} <div class="divider"></div> <div class="inline field"> - <label></label> <button class="ui primary button">{{ctx.Locale.Tr "auth.reset_password_helper"}}</button> {{if and .has_two_factor (not .scratch_code)}} - <a href="{{.Link}}?code={{.Code}}&scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code" | Str2html}}</a> + <a href="?code={{.Code}}&scratch_code=true">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a> {{end}} </div> {{else}} - <p class="center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl) | Str2html}}</p> + <p class="center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl)}}</p> {{end}} </div> </form> diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 40e54ec8fa..9872096fbc 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -9,21 +9,20 @@ {{end}} </h4> <div class="ui attached segment"> - <form class="ui form" action="{{.SignInLink}}" method="post"> + <form class="ui form tw-max-w-2xl tw-m-auto" action="{{.SignInLink}}" method="post"> {{.CsrfTokenHtml}} - <div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> + <div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> <label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label> <input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required> </div> {{if or (not .DisablePassword) .LinkAccountMode}} - <div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> + <div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> <label for="password">{{ctx.Locale.Tr "password"}}</label> <input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required> </div> {{end}} {{if not .LinkAccountMode}} <div class="inline field"> - <label></label> <div class="ui checkbox"> <label>{{ctx.Locale.Tr "auth.remember_me"}}</label> <input name="remember" type="checkbox"> @@ -33,8 +32,7 @@ {{template "user/auth/captcha" .}} - <div class="inline field"> - <label></label> + <div class="field"> <button class="ui primary button"> {{if .LinkAccountMode}} {{ctx.Locale.Tr "auth.oauth_signin_submit"}} @@ -46,9 +44,8 @@ </div> {{if .ShowRegistrationButton}} - <div class="inline field"> - <label></label> - <a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now" | Str2html}}</a> + <div class="field"> + <a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a> </div> {{end}} @@ -56,11 +53,11 @@ <div class="divider divider-text"> {{ctx.Locale.Tr "sign_in_or"}} </div> - <div id="oauth2-login-navigator" class="gt-py-2"> - <div class="gt-df gt-fc gt-jc"> - <div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3"> + <div id="oauth2-login-navigator" class="tw-py-1"> + <div class="tw-flex tw-flex-col tw-justify-center"> + <div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2"> {{range $provider := .OAuth2Providers}} - <a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}"> + <a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center tw-py-2 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}"> {{$provider.IconHTML 28}} {{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}} </a> diff --git a/templates/user/auth/signin_navbar.tmpl b/templates/user/auth/signin_navbar.tmpl index bc7fd03e13..7f52185a7d 100644 --- a/templates/user/auth/signin_navbar.tmpl +++ b/templates/user/auth/signin_navbar.tmpl @@ -1,6 +1,6 @@ {{if or .EnableOpenIDSignIn .EnableSSPI}} -<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar"> - <div class="new-menu-inner"> +<overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar secondary-nav"> + <div class="overflow-menu-items tw-justify-center"> <a class="{{if .PageIsLogin}}active {{end}}item" rel="nofollow" href="{{AppSubUrl}}/user/login"> {{ctx.Locale.Tr "auth.login_userpass"}} </a> @@ -20,5 +20,5 @@ </a> {{end}} </div> -</div> +</overflow-menu> {{end}} diff --git a/templates/user/auth/signin_openid.tmpl b/templates/user/auth/signin_openid.tmpl index 0428026aa8..c1f392dc13 100644 --- a/templates/user/auth/signin_openid.tmpl +++ b/templates/user/auth/signin_openid.tmpl @@ -8,12 +8,12 @@ OpenID </h4> <div class="ui attached segment"> - <form class="ui form" action="{{.Link}}" method="post"> + <form class="ui form tw-m-auto" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <div class="inline field"> {{ctx.Locale.Tr "auth.openid_signin_desc"}} </div> - <div class="required inline field {{if .Err_OpenID}}error{{end}}"> + <div class="required field {{if .Err_OpenID}}error{{end}}"> <label for="openid"> {{svg "fontawesome-openid"}} OpenID URI @@ -21,14 +21,12 @@ <input id="openid" name="openid" value="{{.openid}}" autofocus required> </div> <div class="inline field"> - <label></label> <div class="ui checkbox"> <label>{{ctx.Locale.Tr "auth.remember_me"}}</label> <input name="remember" type="checkbox"> </div> </div> <div class="inline field"> - <label></label> <button class="ui primary button">{{ctx.Locale.Tr "sign_in"}}</button> </div> </form> diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index e930bd3d15..bdb691d833 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -7,7 +7,7 @@ {{end}} </h4> <div class="ui attached segment"> - <form class="ui form" action="{{.SignUpLink}}" method="post"> + <form class="ui form tw-max-w-2xl tw-m-auto" action="{{.SignUpLink}}" method="post"> {{.CsrfTokenHtml}} {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}} {{template "base/alert" .}} @@ -15,21 +15,21 @@ {{if .DisableRegistration}} <p>{{ctx.Locale.Tr "auth.disable_register_prompt"}}</p> {{else}} - <div class="required inline field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> + <div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> <label for="user_name">{{ctx.Locale.Tr "username"}}</label> <input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required> </div> - <div class="required inline field {{if .Err_Email}}error{{end}}"> + <div class="required field {{if .Err_Email}}error{{end}}"> <label for="email">{{ctx.Locale.Tr "email"}}</label> <input id="email" name="email" type="email" value="{{.email}}" required> </div> {{if not .DisablePassword}} - <div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> + <div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> <label for="password">{{ctx.Locale.Tr "password"}}</label> <input id="password" name="password" type="password" value="{{.password}}" autocomplete="new-password" required> </div> - <div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> + <div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> <label for="retype">{{ctx.Locale.Tr "re_type"}}</label> <input id="retype" name="retype" type="password" value="{{.retype}}" autocomplete="new-password" required> </div> @@ -38,7 +38,6 @@ {{template "user/auth/captcha" .}} <div class="inline field"> - <label></label> <button class="ui primary button"> {{if .LinkAccountMode}} {{ctx.Locale.Tr "auth.oauth_signup_submit"}} @@ -50,7 +49,6 @@ {{if not .LinkAccountMode}} <div class="inline field"> - <label></label> <a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.register_helper_msg"}}</a> </div> {{end}} @@ -60,11 +58,11 @@ <div class="divider divider-text"> {{ctx.Locale.Tr "sign_in_or"}} </div> - <div id="oauth2-login-navigator" class="gt-py-2"> - <div class="gt-df gt-fc gt-jc"> - <div id="oauth2-login-navigator-inner" class="gt-df gt-fc gt-fw gt-ac gt-gap-3"> + <div id="oauth2-login-navigator" class="tw-py-1"> + <div class="tw-flex tw-flex-col tw-justify-center"> + <div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2"> {{range $provider := .OAuth2Providers}} - <a class="{{$provider.Name}} ui button gt-df gt-ac gt-jc gt-py-3 oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}"> + <a class="{{$provider.Name}} ui button tw-flex tw-items-center tw-justify-center tw-py-2 tw-w-full oauth-login-link" href="{{AppSubUrl}}/user/oauth2/{{$provider.DisplayName}}"> {{$provider.IconHTML 28}} {{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}} </a> diff --git a/templates/user/auth/signup_openid_navbar.tmpl b/templates/user/auth/signup_openid_navbar.tmpl index 075f2e4d7b..89068ddde1 100644 --- a/templates/user/auth/signup_openid_navbar.tmpl +++ b/templates/user/auth/signup_openid_navbar.tmpl @@ -1,5 +1,5 @@ -<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar"> - <div class="new-menu-inner"> +<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav"> + <div class="overflow-menu-items tw-justify-center"> <a class="{{if .PageIsOpenIDConnect}}active {{end}}item" href="{{AppSubUrl}}/user/openid/connect"> {{ctx.Locale.Tr "auth.openid_connect_title"}} </a> @@ -9,4 +9,4 @@ </a> {{end}} </div> -</div> +</overflow-menu> diff --git a/templates/user/auth/signup_openid_register.tmpl b/templates/user/auth/signup_openid_register.tmpl index 81c36957d1..c017a0e65b 100644 --- a/templates/user/auth/signup_openid_register.tmpl +++ b/templates/user/auth/signup_openid_register.tmpl @@ -7,28 +7,27 @@ {{ctx.Locale.Tr "auth.openid_register_title"}} </h4> <div class="ui attached segment"> - <p> + <p class="tw-max-w-2xl tw-mx-auto"> {{ctx.Locale.Tr "auth.openid_register_desc"}} </p> <form class="ui form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} - <div class="required inline field {{if .Err_UserName}}error{{end}}"> + <div class="required field {{if .Err_UserName}}error{{end}}"> <label for="user_name">{{ctx.Locale.Tr "username"}}</label> <input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required> </div> - <div class="required inline field {{if .Err_Email}}error{{end}}"> + <div class="required field {{if .Err_Email}}error{{end}}"> <label for="email">{{ctx.Locale.Tr "email"}}</label> <input id="email" name="email" type="email" value="{{.email}}" required> </div> {{template "user/auth/captcha" .}} - <div class="inline field"> + <div class="field"> <label for="openid">OpenID URI</label> <input id="openid" value="{{.OpenID}}" readonly> </div> <div class="inline field"> - <label></label> <button class="ui primary button">{{ctx.Locale.Tr "auth.create_new_account"}}</button> </div> </form> diff --git a/templates/user/auth/twofa.tmpl b/templates/user/auth/twofa.tmpl index d325114155..d245239171 100644 --- a/templates/user/auth/twofa.tmpl +++ b/templates/user/auth/twofa.tmpl @@ -2,22 +2,21 @@ <div role="main" aria-label="{{.Title}}" class="page-content user signin"> <div class="ui middle very relaxed page grid"> <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <form class="ui form tw-max-w-2xl tw-m-auto" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <h3 class="ui top attached header"> {{ctx.Locale.Tr "twofa"}} </h3> <div class="ui attached segment"> {{template "base/alert" .}} - <div class="required inline field"> + <div class="required field"> <label for="passcode">{{ctx.Locale.Tr "passcode"}}</label> <input id="passcode" name="passcode" type="text" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" autofocus required> </div> <div class="inline field"> - <label></label> <button class="ui primary button">{{ctx.Locale.Tr "auth.verify"}}</button> - <a href="{{AppSubUrl}}/user/two_factor/scratch">{{ctx.Locale.Tr "auth.use_scratch_code" | Str2html}}</a> + <a href="{{AppSubUrl}}/user/two_factor/scratch">{{ctx.Locale.Tr "auth.use_scratch_code"}}</a> </div> </div> </form> diff --git a/templates/user/auth/twofa_scratch.tmpl b/templates/user/auth/twofa_scratch.tmpl index 1aa044b4a5..23ad77f2a9 100644 --- a/templates/user/auth/twofa_scratch.tmpl +++ b/templates/user/auth/twofa_scratch.tmpl @@ -2,20 +2,19 @@ <div role="main" aria-label="{{.Title}}" class="page-content user signin"> <div class="ui middle very relaxed page grid"> <div class="column"> - <form class="ui form" action="{{.Link}}" method="post"> + <form class="ui form tw-max-w-2xl tw-m-auto" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <h3 class="ui top attached header"> {{ctx.Locale.Tr "twofa_scratch"}} </h3> <div class="ui attached segment"> {{template "base/alert" .}} - <div class="required inline field"> + <div class="required field"> <label for="token">{{ctx.Locale.Tr "auth.scratch_code"}}</label> <input id="token" name="token" type="text" autocomplete="off" autofocus required> </div> <div class="inline field"> - <label></label> <button class="ui primary button">{{ctx.Locale.Tr "auth.verify"}}</button> </div> </div> diff --git a/templates/user/auth/webauthn.tmpl b/templates/user/auth/webauthn.tmpl index 722da02f54..1b84765323 100644 --- a/templates/user/auth/webauthn.tmpl +++ b/templates/user/auth/webauthn.tmpl @@ -10,8 +10,8 @@ {{template "base/alert" .}} <p>{{ctx.Locale.Tr "webauthn_sign_in"}}</p> </div> - <div class="ui attached segment gt-df gt-ac gt-jc gt-gap-2 gt-py-3"> - <div class="is-loading" style="width: 40px; height: 40px"></div> + <div class="ui attached segment tw-flex tw-items-center tw-justify-center tw-gap-1 tw-py-2"> + <div class="is-loading tw-w-[40px] tw-h-[40px]"></div> {{ctx.Locale.Tr "webauthn_press_button"}} </div> {{if .HasTwoFactor}} diff --git a/templates/user/auth/webauthn_error.tmpl b/templates/user/auth/webauthn_error.tmpl index fc6064db76..511ff7c287 100644 --- a/templates/user/auth/webauthn_error.tmpl +++ b/templates/user/auth/webauthn_error.tmpl @@ -1,7 +1,7 @@ -<div id="webauthn-error" class="ui negative message gt-hidden"> +<div id="webauthn-error" class="ui negative message tw-hidden"> <div class="header">{{ctx.Locale.Tr "webauthn_error"}}</div> - <div id="webauthn-error-msg" class="gt-pt-3"></div> - <div class="gt-hidden"> + <div id="webauthn-error-msg" class="tw-pt-2"></div> + <div class="tw-hidden"> <div data-webauthn-error-msg="browser">{{ctx.Locale.Tr "webauthn_unsupported_browser"}}</div> <div data-webauthn-error-msg="unknown">{{ctx.Locale.Tr "webauthn_error_unknown"}}</div> <div data-webauthn-error-msg="insecure">{{ctx.Locale.Tr "webauthn_error_insecure"}}</div> diff --git a/templates/user/code.tmpl b/templates/user/code.tmpl index da9a3c3a24..ff6c69d615 100644 --- a/templates/user/code.tmpl +++ b/templates/user/code.tmpl @@ -1,10 +1,9 @@ {{template "base/head" .}} {{if .ContextUser.IsOrganization}} - <div role="main" aria-label="{{.Title}}" class="page-content repository"> - {{template "shared/user/org_profile_avatar" .}} + <div role="main" aria-label="{{.Title}}" class="page-content organization code"> + {{template "org/header" .}} <div class="ui container"> - {{template "user/overview/header" .}} - {{template "code/searchcombo" .}} + {{template "shared/search/code/search" .}} </div> </div> {{else}} @@ -16,7 +15,7 @@ </div> <div class="ui twelve wide column"> {{template "user/overview/header" .}} - {{template "code/searchcombo" .}} + {{template "shared/search/code/search" .}} </div> </div> </div> diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index a51365e4d6..60aa194534 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -4,79 +4,79 @@ <div class="flex-item-leading"> {{ctx.AvatarUtils.AvatarByAction .}} </div> - <div class="flex-item-main gt-gap-3"> + <div class="flex-item-main tw-gap-2"> <div> {{if gt .ActUser.ID 0}} - <a href="{{AppSubUrl}}/{{(.GetActUserName ctx) | PathEscape}}" title="{{.GetDisplayNameTitle ctx}}">{{.GetDisplayName ctx}}</a> + <a href="{{AppSubUrl}}/{{(.GetActUserName ctx) | PathEscape}}" title="{{.GetActDisplayNameTitle ctx}}">{{.GetActDisplayName ctx}}</a> {{else}} {{.ShortActUserName ctx}} {{end}} {{if .GetOpType.InActions "create_repo"}} - {{ctx.Locale.Tr "action.create_repo" ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.create_repo" (.GetRepoLink ctx) (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "rename_repo"}} - {{ctx.Locale.Tr "action.rename_repo" (.GetContent|Escape) ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.rename_repo" .GetContent (.GetRepoLink ctx) (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "commit_repo"}} {{if .Content}} - {{ctx.Locale.Tr "action.commit_repo" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.commit_repo" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}} {{else}} - {{ctx.Locale.Tr "action.create_branch" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (Escape .GetBranch) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.create_branch" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}} {{end}} {{else if .GetOpType.InActions "create_issue"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.create_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.create_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "create_pull_request"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.create_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.create_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "transfer_repo"}} - {{ctx.Locale.Tr "action.transfer_repo" .GetContent ((.GetRepoLink ctx)|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.transfer_repo" .GetContent (.GetRepoLink ctx) (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "push_tag"}} - {{ctx.Locale.Tr "action.push_tag" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.push_tag" (.GetRepoLink ctx) (.GetRefLink ctx) .GetTag (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "comment_issue"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.comment_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.comment_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "merge_pull_request"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.merge_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.merge_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "close_issue"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.close_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.close_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "reopen_issue"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.reopen_issue" ((printf "%s/issues/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.reopen_issue" (printf "%s/issues/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "close_pull_request"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.close_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.close_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "reopen_pull_request"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.reopen_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.reopen_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "delete_tag"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.delete_tag" ((.GetRepoLink ctx)|Escape) (.GetTag|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.delete_tag" (.GetRepoLink ctx) .GetTag (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "delete_branch"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.delete_branch" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.delete_branch" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "mirror_sync_push"}} - {{ctx.Locale.Tr "action.mirror_sync_push" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.mirror_sync_push" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "mirror_sync_create"}} - {{ctx.Locale.Tr "action.mirror_sync_create" ((.GetRepoLink ctx)|Escape) ((.GetRefLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.mirror_sync_create" (.GetRepoLink ctx) (.GetRefLink ctx) .GetBranch (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "mirror_sync_delete"}} - {{ctx.Locale.Tr "action.mirror_sync_delete" ((.GetRepoLink ctx)|Escape) (.GetBranch|Escape) ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.mirror_sync_delete" (.GetRepoLink ctx) .GetBranch (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "approve_pull_request"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.approve_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.approve_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "reject_pull_request"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.reject_pull_request" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.reject_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "comment_pull"}} {{$index := index .GetIssueInfos 0}} - {{ctx.Locale.Tr "action.comment_pull" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) | Str2html}} + {{ctx.Locale.Tr "action.comment_pull" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{else if .GetOpType.InActions "publish_release"}} {{$linkText := .Content | RenderEmoji $.Context}} - {{ctx.Locale.Tr "action.publish_release" ((.GetRepoLink ctx)|Escape) ((printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag)|Escape) ((.ShortRepoPath ctx)|Escape) $linkText | Str2html}} + {{ctx.Locale.Tr "action.publish_release" (.GetRepoLink ctx) (printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag) (.ShortRepoPath ctx) $linkText}} {{else if .GetOpType.InActions "review_dismissed"}} {{$index := index .GetIssueInfos 0}} {{$reviewer := index .GetIssueInfos 1}} - {{ctx.Locale.Tr "action.review_dismissed" ((printf "%s/pulls/%s" (.GetRepoLink ctx) $index) |Escape) $index ((.ShortRepoPath ctx)|Escape) $reviewer | Str2html}} + {{ctx.Locale.Tr "action.review_dismissed" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx) $reviewer}} {{end}} {{TimeSince .GetCreate ctx.Locale}} </div> @@ -84,7 +84,7 @@ {{$push := ActionContent2Commits .}} {{$repoLink := (.GetRepoLink ctx)}} {{$repo := .Repo}} - <div class="gt-df gt-fc gt-gap-2"> + <div class="tw-flex tw-flex-col tw-gap-1"> {{range $push.Commits}} {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} <div class="flex-text-block"> @@ -107,7 +107,7 @@ <a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | RenderEmoji $.Context | RenderCodeBlock}}</a> {{$comment := index .GetIssueInfos 1}} {{if $comment}} - <div class="markup gt-font-14">{{RenderMarkdownToHtml ctx $comment}}</div> + <div class="markup tw-text-14">{{RenderMarkdownToHtml ctx $comment}}</div> {{end}} {{else if .GetOpType.InActions "merge_pull_request"}} <div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div> @@ -119,7 +119,7 @@ {{end}} </div> <div class="flex-item-trailing"> - {{svg (printf "octicon-%s" (ActionIcon .GetOpType)) 32 "text grey gt-mr-2"}} + {{svg (printf "octicon-%s" (ActionIcon .GetOpType)) 32 "text grey tw-mr-1"}} </div> </div> {{end}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 82622366e7..89f23163f7 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -2,32 +2,33 @@ <div role="main" aria-label="{{.Title}}" class="page-content dashboard issues"> {{template "user/dashboard/navbar" .}} <div class="ui container"> + {{template "base/alert" .}} <div class="flex-container"> <div class="flex-container-nav"> - <div class="ui secondary vertical filter menu gt-bg-transparent"> - <a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> + <div class="ui secondary vertical filter menu tw-bg-transparent"> + <a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="?type=your_repositories&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> {{ctx.Locale.Tr "home.issues.in_your_repos"}} <strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong> </a> - <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> + <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="?type=assigned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> {{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}} <strong>{{CountFmt .IssueStats.AssignCount}}</strong> </a> - <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> + <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="?type=created_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> {{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}} <strong>{{CountFmt .IssueStats.CreateCount}}</strong> </a> {{if .PageIsPulls}} - <a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> + <a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="?type=review_requested&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> {{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}} <strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong> </a> - <a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> + <a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="?type=reviewed_by&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> {{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}} <strong>{{CountFmt .IssueStats.ReviewedCount}}</strong> </a> {{end}} - <a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> + <a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="?type=mentioned&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}"> {{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}} <strong>{{CountFmt .IssueStats.MentionCount}}</strong> </a> @@ -36,12 +37,12 @@ <div class="flex-container-main content"> <div class="list-header"> <div class="small-menu-items ui compact tiny menu list-header-toggle"> - <a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> - {{svg "octicon-issue-opened" 16 "gt-mr-3"}} + <a class="item{{if not .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> + {{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .IssueStats.OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} </a> - <a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}"> - {{svg "octicon-issue-closed" 16 "gt-mr-3"}} + <a class="item{{if .IsShowClosed}} active{{end}}" href="?type={{$.ViewType}}&sort={{$.SortType}}&state=closed&q={{$.Keyword}}"> + {{svg "octicon-issue-closed" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} </a> </div> @@ -50,26 +51,26 @@ <input type="hidden" name="type" value="{{$.ViewType}}"> <input type="hidden" name="sort" value="{{$.SortType}}"> <input type="hidden" name="state" value="{{$.State}}"> - {{template "shared/searchinput" dict "Value" $.Keyword}} - <button id="issue-list-quick-goto" class="ui small icon button gt-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}">{{svg "octicon-hash"}}</button> - <button class="ui small icon button" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button> + {{template "shared/search/input" dict "Value" $.Keyword}} + <button id="issue-list-quick-goto" class="ui small icon button tw-hidden" data-tooltip-content="{{ctx.Locale.Tr "explore.go_to"}}">{{svg "octicon-hash"}}</button> + {{template "shared/search/button"}} </div> </form> <!-- Sort --> <div class="list-header-sort ui small dropdown type jump item"> - <span class="text gt-whitespace-nowrap"> + <span class="text tw-whitespace-nowrap"> {{ctx.Locale.Tr "repo.issues.filter_sort"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} </span> <div class="menu"> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> - <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> - <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> - <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=recentupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastupdate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?type={{$.ViewType}}&sort=latest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?type={{$.ViewType}}&sort=oldest&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=mostcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> + <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?type={{$.ViewType}}&sort=leastcomment&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> + <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=nearduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> + <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?type={{$.ViewType}}&sort=farduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> </div> </div> </div> diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 390457a60a..0f1e866a21 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -4,7 +4,7 @@ <div class="ui container"> <div class="flex-container"> <div class="flex-container-nav"> - <div class="ui secondary vertical filter menu gt-bg-transparent"> + <div class="ui secondary vertical filter menu tw-bg-transparent"> <div class="item"> {{ctx.Locale.Tr "home.issues.in_your_repos"}} <strong>{{.Total}}</strong> @@ -12,7 +12,7 @@ <div class="divider"></div> {{range .Repos}} {{with $Repo := .}} - <a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="{{$.Link}}?repos=[ + <a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}active{{end}}{{end}} repo name item" href="?repos=[ {{- with $include := true -}} {{- range $.RepoIDs -}} {{- if eq . $Repo.ID -}} @@ -36,24 +36,21 @@ <div class="flex-container-main content"> <div class="list-header"> <div class="small-menu-items ui compact tiny menu list-header-toggle"> - <a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> - {{svg "octicon-milestone" 16 "gt-mr-3"}} + <a class="item{{if not .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> + {{svg "octicon-milestone" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .MilestoneStats.OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} </a> - <a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}"> - {{svg "octicon-check" 16 "gt-mr-3"}} + <a class="item{{if .IsShowClosed}} active{{end}}" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}"> + {{svg "octicon-check" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .MilestoneStats.ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} </a> </div> <form class="list-header-search ui form ignore-dirty"> - <div class="ui small search fluid action input"> - <input type="hidden" name="type" value="{{$.ViewType}}"> + <input type="hidden" name="type" value="{{$.ViewType}}"> <input type="hidden" name="repos" value="[{{range $.RepoIDs}}{{.}},{{end}}]"> <input type="hidden" name="sort" value="{{$.SortType}}"> <input type="hidden" name="state" value="{{$.State}}"> - {{template "shared/searchinput" dict "Value" $.Keyword}} - <button class="ui small icon button" type="submit" aria-label="{{ctx.Locale.Tr "explore.search"}}">{{svg "octicon-search"}}</button> - </div> + {{template "shared/search/combo" dict "Value" $.Keyword}} </form> <!-- Sort --> <div class="list-header-sort ui dropdown type jump item"> @@ -62,12 +59,12 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.closest_due_date"}}</a> - <a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.furthest_due_date"}}</a> - <a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a> - <a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a> - <a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a> - <a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a> + <a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a> + <a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a> + <a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a> + <a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a> + <a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a> + <a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a> </div> </div> </div> @@ -75,15 +72,15 @@ {{range .Milestones}} <li class="milestone-card"> <div class="milestone-header"> - <h3 class="flex-text-block gt-m-0"> + <h3 class="flex-text-block tw-m-0"> <span class="ui large label"> {{.Repo.FullName}} </span> {{svg "octicon-milestone" 16}} <a class="muted" href="{{.Repo.Link}}/milestone/{{.ID}}">{{.Name}}</a> </h3> - <div class="gt-df gt-ac"> - <span class="gt-mr-3">{{.Completeness}}%</span> + <div class="tw-flex tw-items-center"> + <span class="tw-mr-2">{{.Completeness}}%</span> <progress value="{{.Completeness}}" max="100"></progress> </div> </div> @@ -106,14 +103,14 @@ {{if .UpdatedUnix}} <div class="flex-text-block"> {{svg "octicon-clock"}} - {{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale) | Safe}} + {{ctx.Locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix ctx.Locale)}} </div> {{end}} <div class="flex-text-block"> {{if .IsClosed}} {{$closedDate:= TimeSinceUnix .ClosedDateUnix ctx.Locale}} {{svg "octicon-clock" 14}} - {{ctx.Locale.Tr "repo.milestones.closed" $closedDate | Safe}} + {{ctx.Locale.Tr "repo.milestones.closed" $closedDate}} {{else}} {{if .DeadlineString}} <span{{if .IsOverdue}} class="text red"{{end}}> @@ -141,7 +138,7 @@ </div> {{if .Content}} <div class="markup content"> - {{.RenderedContent|Str2html}} + {{.RenderedContent}} </div> {{end}} </li> diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl index af07897e2c..464228289e 100644 --- a/templates/user/dashboard/navbar.tmpl +++ b/templates/user/dashboard/navbar.tmpl @@ -1,15 +1,15 @@ -<div class="dashboard-navbar"> +<div class="secondary-nav tw-border-b tw-border-b-secondary"> <div class="ui secondary stackable menu"> <div class="item"> <div class="ui floating dropdown jump"> <span class="text truncated-item-container"> - {{ctx.AvatarUtils.Avatar .ContextUser}} + {{ctx.AvatarUtils.Avatar .ContextUser 24 "tw-mr-1"}} <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> <span class="org-visibility"> {{if .ContextUser.Visibility.IsLimited}}<div class="ui basic tiny horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</div>{{end}} {{if .ContextUser.Visibility.IsPrivate}}<div class="ui basic tiny horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</div>{{end}} </span> - {{svg "octicon-triangle-down" 14 "dropdown icon"}} + {{svg "octicon-triangle-down" 14 "dropdown icon tw-ml-1"}} </span> <div class="context user overflow menu"> <div class="ui header"> @@ -78,7 +78,7 @@ {{if .ContextUser.IsOrganization}} <div class="right menu"> - <a class="{{if .PageIsNews}}active {{end}}item gt-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}"> + <a class="{{if .PageIsNews}}active {{end}}item tw-ml-auto" href="{{.ContextUser.DashboardLink}}{{if .Team}}/{{PathEscape .Team.Name}}{{end}}"> {{svg "octicon-rss"}} {{ctx.Locale.Tr "activities"}} </a> {{if not .UnitIssuesGlobalDisabled}} @@ -105,4 +105,3 @@ {{end}} </div> </div> -<div class="divider"></div> diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index ed6f906822..a879f1fb9d 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -9,7 +9,7 @@ const data = { textOrganization: {{ctx.Locale.Tr "organization"}}, textMyRepos: {{ctx.Locale.Tr "home.my_repos"}}, textNewRepo: {{ctx.Locale.Tr "new_repo"}}, - textSearchRepos: {{ctx.Locale.Tr "home.search_repos"}}, + textSearchRepos: {{ctx.Locale.Tr "search.repo_kind"}}, textFilter: {{ctx.Locale.Tr "home.filter"}}, textShowArchived: {{ctx.Locale.Tr "home.show_archived"}}, textShowPrivate: {{ctx.Locale.Tr "home.show_private"}}, diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl index d8f8d462d3..bf3b51ee3b 100644 --- a/templates/user/notification/notification_div.tmpl +++ b/templates/user/notification/notification_div.tmpl @@ -1,11 +1,11 @@ <div role="main" aria-label="{{.Title}}" class="page-content user notification" id="notification_div" data-sequence-number="{{.SequenceNumber}}"> <div class="ui container"> {{$notificationUnreadCount := call .NotificationUnreadCount}} - <div class="gt-df gt-ac gt-sb gt-mb-4"> + <div class="tw-flex tw-items-center tw-justify-between tw-mb-[--page-spacing]"> <div class="small-menu-items ui compact tiny menu"> <a class="{{if eq .Status 1}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=unread"> {{ctx.Locale.Tr "notification.unread"}} - <div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}gt-hidden{{end}}">{{$notificationUnreadCount}}</div> + <div class="notifications-unread-count ui label {{if not $notificationUnreadCount}}tw-hidden{{end}}">{{$notificationUnreadCount}}</div> </a> <a class="{{if eq .Status 2}}active {{end}}item" href="{{AppSubUrl}}/notifications?q=read"> {{ctx.Locale.Tr "notification.read"}} @@ -14,19 +14,19 @@ {{if and (eq .Status 1)}} <form action="{{AppSubUrl}}/notifications/purge" method="post"> {{$.CsrfTokenHtml}} - <div class="{{if not $notificationUnreadCount}}gt-hidden{{end}}"> - <button class="ui mini button primary gt-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}"> + <div class="{{if not $notificationUnreadCount}}tw-hidden{{end}}"> + <button class="ui mini button primary tw-mr-0" title="{{ctx.Locale.Tr "notification.mark_all_as_read"}}"> {{svg "octicon-checklist"}} </button> </div> </form> {{end}} </div> - <div class="gt-p-0"> + <div class="tw-p-0"> <div id="notification_table"> {{if not .Notifications}} - <div class="gt-df gt-ac gt-fc gt-p-4"> - {{svg "octicon-inbox" 56 "gt-mb-4"}} + <div class="tw-flex tw-items-center tw-flex-col tw-p-4"> + {{svg "octicon-inbox" 56 "tw-mb-4"}} {{if eq .Status 1}} {{ctx.Locale.Tr "notification.no_unread"}} {{else}} @@ -35,22 +35,22 @@ </div> {{else}} {{range $notification := .Notifications}} - <div class="notifications-item gt-df gt-ac gt-fw gt-gap-3 gt-p-3" id="notification_{{.ID}}" data-status="{{.Status}}"> - <div class="notifications-icon gt-ml-3 gt-mr-2 gt-self-start gt-mt-2"> + <div class="notifications-item tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-p-2" id="notification_{{.ID}}" data-status="{{.Status}}"> + <div class="notifications-icon tw-ml-2 tw-mr-1 tw-self-start tw-mt-1"> {{if .Issue}} {{template "shared/issueicon" .Issue}} {{else}} {{svg "octicon-repo" 16 "text grey"}} {{end}} </div> - <a class="notifications-link gt-df gt-f1 gt-fc silenced" href="{{.Link ctx}}"> - <div class="notifications-top-row gt-font-13"> + <a class="notifications-link tw-flex tw-flex-1 tw-flex-col silenced" href="{{.Link ctx}}"> + <div class="notifications-top-row tw-text-13"> {{.Repository.FullName}} {{if .Issue}}<span class="text light-3">#{{.Issue.Index}}</span>{{end}} {{if eq .Status 3}} - {{svg "octicon-pin" 13 "text blue gt-mt-1 gt-ml-2"}} + {{svg "octicon-pin" 13 "text blue tw-mt-0.5 tw-ml-1"}} {{end}} </div> - <div class="notifications-bottom-row gt-font-16 gt-py-1"> + <div class="notifications-bottom-row tw-text-16 tw-py-0.5"> <span class="issue-title"> {{if .Issue}} {{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}} @@ -60,20 +60,20 @@ </span> </div> </a> - <div class="notifications-updated gt-ac gt-mr-3"> + <div class="notifications-updated tw-items-center tw-mr-2"> {{if .Issue}} {{TimeSinceUnix .Issue.UpdatedUnix ctx.Locale}} {{else}} {{TimeSinceUnix .UpdatedUnix ctx.Locale}} {{end}} </div> - <div class="notifications-buttons gt-ac gt-je gt-gap-2 gt-px-2"> + <div class="notifications-buttons tw-items-center tw-justify-end tw-gap-1 tw-px-1"> {{if ne .Status 3}} <form action="{{AppSubUrl}}/notifications/status" method="post"> {{$.CsrfTokenHtml}} <input type="hidden" name="notification_id" value="{{.ID}}"> <input type="hidden" name="status" value="pinned"> - <button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.pin"}}" + <button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.pin"}}" data-url="{{AppSubUrl}}/notifications/status" data-status="pinned" data-page="{{$.Page.Paginater.Current}}" @@ -89,7 +89,7 @@ <input type="hidden" name="notification_id" value="{{.ID}}"> <input type="hidden" name="status" value="read"> <input type="hidden" name="page" value="{{$.Page.Paginater.Current}}"> - <button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.mark_as_read"}}" + <button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_read"}}" data-url="{{AppSubUrl}}/notifications/status" data-status="read" data-page="{{$.Page.Paginater.Current}}" @@ -104,7 +104,7 @@ <input type="hidden" name="notification_id" value="{{.ID}}"> <input type="hidden" name="status" value="unread"> <input type="hidden" name="page" value="{{$.Page.Paginater.Current}}"> - <button class="btn interact-bg gt-p-3" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}" + <button class="btn interact-bg tw-p-2" title="{{ctx.Locale.Tr "notification.mark_as_unread"}}" data-url="{{AppSubUrl}}/notifications/status" data-status="unread" data-page="{{$.Page.Paginater.Current}}" diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl index ec40d3afea..a5a965ca52 100644 --- a/templates/user/notification/notification_subscriptions.tmpl +++ b/templates/user/notification/notification_subscriptions.tmpl @@ -11,23 +11,23 @@ </div> <div class="ui bottom attached active tab segment"> {{if eq .Status 1}} - <div class="gt-df gt-sb"> - <div class="gt-df"> + <div class="tw-flex tw-justify-between"> + <div class="tw-flex"> <div class="small-menu-items ui compact tiny menu"> - <a class="{{if eq .State "all"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}"> + <a class="{{if eq .State "all"}}active {{end}}item" href="?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}"> {{ctx.Locale.Tr "all"}} </a> - <a class="{{if eq .State "open"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}"> - {{svg "octicon-issue-opened" 16 "gt-mr-3"}} + <a class="{{if eq .State "open"}}active {{end}}item" href="?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}"> + {{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.open_title"}} </a> - <a class="{{if eq .State "closed"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}"> - {{svg "octicon-issue-closed" 16 "gt-mr-3"}} + <a class="{{if eq .State "closed"}}active {{end}}item" href="?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}"> + {{svg "octicon-issue-closed" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.issues.closed_title"}} </a> </div> </div> - <div class="gt-df gt-sb"> + <div class="tw-flex tw-justify-between"> <div class="ui right aligned secondary filter menu labels"> <!-- Type --> <div class="ui dropdown type jump item"> @@ -36,9 +36,9 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{ctx.Locale.Tr "all"}}</a> - <a class="{{if eq .IssueType "issues"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{ctx.Locale.Tr "issues"}}</a> - <a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{ctx.Locale.Tr "pull_requests"}}</a> + <a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{ctx.Locale.Tr "all"}}</a> + <a class="{{if eq .IssueType "issues"}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{ctx.Locale.Tr "issues"}}</a> + <a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{ctx.Locale.Tr "pull_requests"}}</a> </div> </div> @@ -49,14 +49,14 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> - <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> - <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> - <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> - <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> - <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> - <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> - <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> + <a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a> + <a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a> + <a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> + <a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> + <a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> + <a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> + <a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> + <a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a> </div> </div> </div> @@ -69,7 +69,7 @@ {{template "shared/issuelist" dict "." . "listType" "dashboard"}} {{end}} {{else}} - {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} {{end}} diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index c0cbe2561c..275c4e295e 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -1,72 +1,50 @@ -<div class="ui secondary stackable pointing menu"> - {{if and .HasProfileReadme .ContextUser.IsIndividual}} - <a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview"> - {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} - </a> - {{end}} - <a class="{{if eq .TabName "repositories"}}active {{end}} item" href="{{.ContextUser.HomeLink}}?tab=repositories"> - {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} - {{if .RepoCount}} - <div class="ui small label">{{.RepoCount}}</div> - {{end}} - </a> - {{if or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadProjects)}} - <a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item"> - {{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}} - {{if .ProjectCount}} - <div class="ui small label">{{.ProjectCount}}</div> - {{end}} - </a> - {{end}} - {{if and .IsPackageEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadPackages))}} - <a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item"> - {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} +<overflow-menu class="ui secondary pointing tabular borderless menu"> + <div class="overflow-menu-items"> + {{if and .HasProfileReadme .ContextUser.IsIndividual}} + <a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview"> + {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} </a> - {{end}} - {{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadCode))}} - <a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item"> - {{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}} + {{end}} + <a class="{{if eq .TabName "repositories"}}active {{end}} item" href="{{.ContextUser.HomeLink}}?tab=repositories"> + {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} + {{if .RepoCount}} + <div class="ui small label">{{.RepoCount}}</div> + {{end}} </a> - {{end}} - - {{if .ContextUser.IsOrganization}} - {{if .NumMembers}} - <a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members"> - {{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}} - <div class="ui small label">{{.NumMembers}}</div> + {{if or .ContextUser.IsIndividual .CanReadProjects}} + <a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if .PageIsViewProjects}}active {{end}}item"> + {{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}} + {{if .ProjectCount}} + <div class="ui small label">{{.ProjectCount}}</div> + {{end}} + </a> + {{end}} + {{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}} + <a href="{{.ContextUser.HomeLink}}/-/packages" class="{{if .IsPackagesPage}}active {{end}}item"> + {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} </a> {{end}} - {{if .IsOrganizationMember}} - <a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams"> - {{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}} - {{if .NumTeams}} - <div class="ui small label">{{.NumTeams}}</div> - {{end}} + {{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual .CanReadCode)}} + <a href="{{.ContextUser.HomeLink}}/-/code" class="{{if .IsCodePage}}active {{end}}item"> + {{svg "octicon-code"}} {{ctx.Locale.Tr "user.code"}} </a> {{end}} - - {{if .IsOrganizationOwner}} - <div class="right menu"> - <a class="item" href="{{.OrgLink}}/settings"> - {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} - </a> - </div> - {{end}} - {{else}} - <a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity"> - {{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}} - </a> - {{if not .DisableStars}} + {{if .ContextUser.IsIndividual}} + <a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity"> + {{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}} + </a> + {{if not .DisableStars}} <a class="{{if eq .TabName "stars"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=stars"> {{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}} {{if .ContextUser.NumStars}} <div class="ui small label">{{.ContextUser.NumStars}}</div> {{end}} </a> - {{else}} + {{else}} <a class="{{if eq .TabName "watching"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=watching"> {{svg "octicon-eye"}} {{ctx.Locale.Tr "user.watched"}} </a> + {{end}} {{end}} - {{end}} -</div> + </div> +</overflow-menu> diff --git a/templates/user/overview/package_versions.tmpl b/templates/user/overview/package_versions.tmpl index 6f740e0e7c..0ac2db0d86 100644 --- a/templates/user/overview/package_versions.tmpl +++ b/templates/user/overview/package_versions.tmpl @@ -1,10 +1,9 @@ {{template "base/head" .}} {{if .ContextUser.IsOrganization}} - <div role="main" aria-label="{{.Title}}" class="page-content repository packages"> - {{template "shared/user/org_profile_avatar" .}} + <div role="main" aria-label="{{.Title}}" class="page-content organization packages"> + {{template "org/header" .}} <div class="ui container"> - {{template "user/overview/header" .}} - {{template "package/shared/versionlist" .}} + {{template "package/shared/versionlist" .}} </div> </div> {{else}} @@ -14,11 +13,9 @@ <div class="ui four wide column"> {{template "shared/user/profile_big_avatar" .}} </div> - <div class="ui twelve wide column"> - <div class="gt-mb-4"> + <div class="ui twelve wide column tw-mb-4"> {{template "user/overview/header" .}} - </div> - {{template "package/shared/versionlist" .}} + {{template "package/shared/versionlist" .}} </div> </div> </div> diff --git a/templates/user/overview/packages.tmpl b/templates/user/overview/packages.tmpl index 4fd17696d1..bb2238b919 100644 --- a/templates/user/overview/packages.tmpl +++ b/templates/user/overview/packages.tmpl @@ -1,10 +1,9 @@ {{template "base/head" .}} {{if .ContextUser.IsOrganization}} - <div role="main" aria-label="{{.Title}}" class="page-content repository packages"> - {{template "shared/user/org_profile_avatar" .}} + <div role="main" aria-label="{{.Title}}" class="page-content organization packages"> + {{template "org/header" .}} <div class="ui container"> - {{template "user/overview/header" .}} - {{template "package/shared/list" .}} + {{template "package/shared/list" .}} </div> </div> {{else}} @@ -14,11 +13,9 @@ <div class="ui four wide column"> {{template "shared/user/profile_big_avatar" .}} </div> - <div class="ui twelve wide column"> - <div class="gt-mb-4"> + <div class="ui twelve wide column tw-mb-4"> {{template "user/overview/header" .}} - </div> - {{template "package/shared/list" .}} + {{template "package/shared/list" .}} </div> </div> </div> diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 426b5f042a..cf61bb906a 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -5,11 +5,8 @@ <div class="ui four wide column"> {{template "shared/user/profile_big_avatar" .}} </div> - <div class="ui twelve wide column"> - <div class="gt-mb-4"> - {{template "user/overview/header" .}} - </div> - + <div class="ui twelve wide column tw-mb-4"> + {{template "user/overview/header" .}} {{if eq .TabName "activity"}} {{if .ContextUser.KeepActivityPrivate}} <div class="ui info message"> @@ -20,7 +17,7 @@ {{template "user/dashboard/feeds" .}} {{else if eq .TabName "stars"}} <div class="stars"> - {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} </div> @@ -29,9 +26,9 @@ {{else if eq .TabName "followers"}} {{template "repo/user_cards" .}} {{else if eq .TabName "overview"}} - <div id="readme_profile" class="markup">{{.ProfileReadme | Str2html}}</div> + <div id="readme_profile" class="markup">{{.ProfileReadme}}</div> {{else}} - {{template "explore/repo_search" .}} + {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} {{end}} diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 7c6fd49a08..040f46e48b 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -42,11 +42,11 @@ <div class="ui list"> {{if $.EnableNotifyMail}} <div class="item"> - <div class="gt-mb-3">{{ctx.Locale.Tr "settings.email_desc"}}</div> + <div class="tw-mb-2">{{ctx.Locale.Tr "settings.email_desc"}}</div> <form action="{{AppSubUrl}}/user/settings/account/email" class="ui form" method="post"> {{$.CsrfTokenHtml}} <input name="_method" type="hidden" value="NOTIFICATION"> - <div class="gt-df gt-fw gt-gap-3"> + <div class="tw-flex tw-flex-wrap tw-gap-2"> <div class="ui selection dropdown"> <input name="preference" type="hidden" value="{{.EmailNotificationsPreference}}"> {{svg "octicon-triangle-down" 14 "dropdown icon"}} @@ -96,7 +96,7 @@ </form> </div> {{end}} - <div class="content gt-py-3"> + <div class="content tw-py-2"> <strong>{{.Email}}</strong> {{if .IsPrimary}} <div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div> @@ -128,14 +128,15 @@ {{end}} </div> + {{if not ($.UserDisabledFeatures.Contains "deletion")}} <h4 class="ui top attached error header"> {{ctx.Locale.Tr "settings.delete_account"}} </h4> <div class="ui attached error segment"> <div class="ui red message"> - <p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "settings.delete_prompt" | Str2html}}</p> + <p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "settings.delete_prompt"}}</p> {{if .UserDeleteWithComments}} - <p class="text left gt-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime | Str2html}}</p> + <p class="text left tw-font-semibold">{{ctx.Locale.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime}}</p> {{end}} </div> <form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post"> @@ -151,7 +152,18 @@ </button> </div> </form> + <div class="ui g-modal-confirm delete modal" id="delete-account"> + <div class="header"> + {{svg "octicon-trash"}} + {{ctx.Locale.Tr "settings.delete_account_title"}} + </div> + <div class="content"> + <p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p> + </div> + {{template "base/modal_actions_confirm" .}} + </div> </div> + {{end}} </div> <div class="ui g-modal-confirm delete modal" id="delete-email"> @@ -165,15 +177,4 @@ {{template "base/modal_actions_confirm" .}} </div> -<div class="ui g-modal-confirm delete modal" id="delete-account"> - <div class="header"> - {{svg "octicon-trash"}} - {{ctx.Locale.Tr "settings.delete_account_title"}} - </div> - <div class="content"> - <p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p> - </div> - {{template "base/modal_actions_confirm" .}} -</div> - {{template "user/settings/layout_footer" .}} diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index 7553c798dc..5e2ffc3bb3 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -18,7 +18,7 @@ <div class="flex-item-main"> <details> <summary><span class="flex-item-title">{{.Name}}</span></summary> - <p class="gt-my-2"> + <p class="tw-my-1"> {{ctx.Locale.Tr "settings.repo_and_org_access"}}: {{if .DisplayPublicOnly}} {{ctx.Locale.Tr "settings.permissions_public_only"}} @@ -26,8 +26,8 @@ {{ctx.Locale.Tr "settings.permissions_access_all"}} {{end}} </p> - <p class="gt-my-2">{{ctx.Locale.Tr "settings.permissions_list"}}</p> - <ul class="gt-my-2"> + <p class="tw-my-1">{{ctx.Locale.Tr "settings.permissions_list"}}</p> + <ul class="tw-my-1"> {{range .Scope.StringSlice}} {{if (ne . $.AccessTokenScopePublicOnly)}} <li>{{.}}</li> @@ -36,12 +36,12 @@ </ul> </details> <div class="flex-item-body"> - <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i> + <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i> </div> </div> <div class="flex-item-trailing"> <button class="ui red tiny button delete-button" data-modal-id="delete-token" data-url="{{$.Link}}/delete" data-id="{{.ID}}"> - {{svg "octicon-trash" 16 "gt-mr-2"}} + {{svg "octicon-trash" 16 "tw-mr-1"}} {{ctx.Locale.Tr "settings.delete_token"}} </button> </div> @@ -61,21 +61,21 @@ </div> <div class="field"> <label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label> - <label class="gt-cursor-pointer"> - <input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}"> + <label class="tw-cursor-pointer"> + <input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}"> {{ctx.Locale.Tr "settings.permissions_public_only"}} </label> - <label class="gt-cursor-pointer"> - <input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="" checked> + <label class="tw-cursor-pointer"> + <input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="" checked> {{ctx.Locale.Tr "settings.permissions_access_all"}} </label> </div> <details class="ui optional field"> - <summary class="gt-pb-4 gt-pl-2"> + <summary class="tw-pb-4 tw-pl-1"> {{ctx.Locale.Tr "settings.select_permissions"}} </summary> <p class="activity meta"> - <i>{{ctx.Locale.Tr "settings.access_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i> + <i>{{ctx.Locale.Tr "settings.access_token_desc" (`href="/api/swagger" target="_blank"`|SafeHTML) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i> </p> <div class="scoped-access-token-mount"> <scoped-access-token-selector @@ -90,7 +90,7 @@ {{ctx.Locale.Tr "settings.generate_token"}} </button> </form>{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}} - <div id="scoped-access-warning" class="ui warning message center gt-hidden"> + <div id="scoped-access-warning" class="ui warning message center tw-hidden"> {{ctx.Locale.Tr "settings.at_least_one_permission"}} </div> </div> diff --git a/templates/user/settings/applications_oauth2_edit_form.tmpl b/templates/user/settings/applications_oauth2_edit_form.tmpl index c0bddd55b3..f7ef115693 100644 --- a/templates/user/settings/applications_oauth2_edit_form.tmpl +++ b/templates/user/settings/applications_oauth2_edit_form.tmpl @@ -26,7 +26,7 @@ <form class="ui form ignore-dirty" action="{{.FormActionPath}}/regenerate_secret" method="post"> {{.CsrfTokenHtml}} {{ctx.Locale.Tr "settings.oauth2_regenerate_secret_hint"}} - <button class="ui mini button gt-ml-3" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button> + <button class="ui mini button tw-ml-2" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button> </form> </div> </div> diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl index bf6b28ec5f..cfcb6d053d 100644 --- a/templates/user/settings/applications_oauth2_list.tmpl +++ b/templates/user/settings/applications_oauth2_list.tmpl @@ -4,7 +4,7 @@ {{ctx.Locale.Tr "settings.oauth2_application_create_description"}} </div> {{range .Applications}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> {{svg "octicon-apps" 32}} </div> @@ -21,12 +21,12 @@ <span class="ui basic label" data-tooltip-content="{{ctx.Locale.Tr "settings.oauth2_application_locked"}}">{{ctx.Locale.Tr "locked"}}</span> {{else}} <a href="{{$.Link}}/oauth2/{{.ID}}" class="ui primary tiny button"> - {{svg "octicon-pencil" 16 "gt-mr-2"}} + {{svg "octicon-pencil" 16 "tw-mr-1"}} {{ctx.Locale.Tr "settings.oauth2_application_edit"}} </a> <button class="ui red tiny button delete-button" data-modal-id="remove-gitea-oauth2-application" data-url="{{$.Link}}/oauth2/{{.ID}}/delete"> - {{svg "octicon-trash" 16 "gt-mr-2"}} + {{svg "octicon-trash" 16 "tw-mr-1"}} {{ctx.Locale.Tr "settings.delete_key"}} </button> {{end}} diff --git a/templates/user/settings/blocked_users.tmpl b/templates/user/settings/blocked_users.tmpl new file mode 100644 index 0000000000..e495b85f58 --- /dev/null +++ b/templates/user/settings/blocked_users.tmpl @@ -0,0 +1,5 @@ +{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings blocked_users")}} + <div class="user-setting-content"> + {{template "shared/user/blocked_users" .}} + </div> +{{template "user/settings/layout_footer" .}} diff --git a/templates/user/settings/grants_oauth2.tmpl b/templates/user/settings/grants_oauth2.tmpl index 3c4c6e80d4..b5ae3e0337 100644 --- a/templates/user/settings/grants_oauth2.tmpl +++ b/templates/user/settings/grants_oauth2.tmpl @@ -14,7 +14,7 @@ <div class="flex-item-main"> <div class="flex-item-title">{{.Application.Name}}</div> <div class="flex-item-body"> - <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}</i> + <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}</i> </div> </div> <div class="flex-item-trailing"> @@ -29,7 +29,7 @@ <div class="ui g-modal-confirm delete modal" id="revoke-gitea-oauth2-grant"> <div class="header"> - {{svg "octicon-shield" 16 "gt-mr-2"}} + {{svg "octicon-shield" 16 "tw-mr-1"}} {{ctx.Locale.Tr "settings.revoke_oauth2_grant"}} </div> <div class="content"> diff --git a/templates/user/settings/keys.tmpl b/templates/user/settings/keys.tmpl index 93037e7e28..e0f5e426ae 100644 --- a/templates/user/settings/keys.tmpl +++ b/templates/user/settings/keys.tmpl @@ -1,7 +1,11 @@ {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings sshkeys")}} <div class="user-setting-content"> - {{template "user/settings/keys_ssh" .}} + {{if not ($.UserDisabledFeatures.Contains "manage_ssh_keys")}} + {{template "user/settings/keys_ssh" .}} + {{end}} {{template "user/settings/keys_principal" .}} + {{if not ($.UserDisabledFeatures.Contains "manage_gpg_keys")}} {{template "user/settings/keys_gpg" .}} + {{end}} </div> {{template "user/settings/layout_footer" .}} diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index 481d7482b4..d86d838f18 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -5,12 +5,12 @@ </div> </h4> <div class="ui attached segment"> - <div class="{{if not .HasGPGError}}gt-hidden{{end}} gt-mb-4" id="add-gpg-key-panel"> + <div class="{{if not .HasGPGError}}tw-hidden{{end}} tw-mb-4" id="add-gpg-key-panel"> <form class="ui form{{if .HasGPGError}} error{{end}}" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <input type="hidden" name="title" value="none"> <div class="field {{if .Err_Content}}error{{end}}"> - <label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label> + <label for="gpg-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label> <textarea id="gpg-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea> </div> {{if .Err_Signature}} @@ -22,11 +22,11 @@ <input readonly="" value="{{.TokenToSign}}"> <div class="help"> <p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p> - <p><code>{{ctx.Locale.Tr "settings.gpg_token_code" .TokenToSign .PaddedKeyID}}</code></p> + <p><code>{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}</code></p> </div> </div> <div class="field"> - <label for="signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label> + <label for="gpg-key-signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label> <textarea id="gpg-key-signature" name="signature" placeholder="{{ctx.Locale.Tr "settings.key_signature_gpg_placeholder"}}" required>{{.signature}}</textarea> </div> {{end}} @@ -43,7 +43,7 @@ <div class="flex-item"> <p> {{ctx.Locale.Tr "settings.gpg_desc"}}<br> - {{ctx.Locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification" | Str2html}} + {{ctx.Locale.Tr "settings.gpg_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification#gpg-commit-signature-verification"}} </p> </div> {{range .GPGKeys}} @@ -63,9 +63,9 @@ <b>{{ctx.Locale.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.PaddedKeyID}} {{end}} </div> <div class="flex-item-body"> - <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .AddedUnix) | Safe}}</i> + <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .AddedUnix)}}</i> - - <i>{{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateTime "short" .ExpiredUnix) | Safe}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}</i> + <i>{{if not .ExpiredUnix.IsZero}}{{ctx.Locale.Tr "settings.valid_until_date" (DateTime "short" .ExpiredUnix)}}{{else}}{{ctx.Locale.Tr "settings.valid_forever"}}{{end}}</i> </div> </div> <div class="flex-item-trailing"> @@ -73,7 +73,7 @@ {{ctx.Locale.Tr "settings.delete_key"}} </button> {{if and (not .Verified) (ne $.VerifyingID .KeyID)}} - <a class="ui primary tiny button" href="{{$.Link}}?verify_gpg={{.KeyID}}">{{ctx.Locale.Tr "settings.gpg_key_verify"}}</a> + <a class="ui primary tiny button" href="?verify_gpg={{.KeyID}}">{{ctx.Locale.Tr "settings.gpg_key_verify"}}</a> {{end}} </div> </div> @@ -90,7 +90,7 @@ <input readonly="" value="{{$.TokenToSign}}"> <div class="help"> <p>{{ctx.Locale.Tr "settings.gpg_token_help"}}</p> - <p><code>{{ctx.Locale.Tr "settings.gpg_token_code" $.TokenToSign .PaddedKeyID}}</code></p> + <p><code>{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}</code></p> </div> <br> </div> diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl index 513afc2b61..37d8fb0e95 100644 --- a/templates/user/settings/keys_principal.tmpl +++ b/templates/user/settings/keys_principal.tmpl @@ -22,7 +22,7 @@ <div class="flex-item-main"> <div class="flex-item-title">{{.Name}}</div> <div class="flex-item-body"> - <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info" 16}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i> + <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} — {{svg "octicon-info" 16}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i> </div> </div> <div class="flex-item-trailing"> @@ -36,7 +36,7 @@ </div> <br> - <div {{if not .HasPrincipalError}}class="gt-hidden"{{end}} id="add-ssh-principal-panel"> + <div {{if not .HasPrincipalError}}class="tw-hidden"{{end}} id="add-ssh-principal-panel"> <h4 class="ui top attached header"> {{ctx.Locale.Tr "settings.add_new_principal"}} </h4> @@ -44,7 +44,7 @@ <form class="ui form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <div class="field {{if .Err_Content}}error{{end}}"> - <label for="content">{{ctx.Locale.Tr "settings.principal_content"}}</label> + <label for="ssh-principal-content">{{ctx.Locale.Tr "settings.principal_content"}}</label> <input id="ssh-principal-content" name="content" value="{{.content}}" autofocus required> </div> <input name="title" type="hidden" value="principal"> diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl index fc8b70ea28..d31cc81b66 100644 --- a/templates/user/settings/keys_ssh.tmpl +++ b/templates/user/settings/keys_ssh.tmpl @@ -7,15 +7,15 @@ </div> </h4> <div class="ui attached segment"> - <div class="{{if not .HasSSHError}}gt-hidden{{end}} gt-mb-4" id="add-ssh-key-panel"> + <div class="{{if not .HasSSHError}}tw-hidden{{end}} tw-mb-4" id="add-ssh-key-panel"> <form class="ui form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <div class="field {{if .Err_Title}}error{{end}}"> - <label for="title">{{ctx.Locale.Tr "settings.key_name"}}</label> + <label for="ssh-key-title">{{ctx.Locale.Tr "settings.key_name"}}</label> <input id="ssh-key-title" name="title" value="{{.title}}" autofocus required maxlength="50"> </div> <div class="field {{if .Err_Content}}error{{end}}"> - <label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label> + <label for="ssh-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label> <textarea id="ssh-key-content" name="content" class="js-quick-submit" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea> </div> <input name="type" type="hidden" value="ssh"> @@ -31,7 +31,7 @@ <div class="flex-item"> <p> {{ctx.Locale.Tr "settings.ssh_desc"}}<br> - {{ctx.Locale.Tr "settings.ssh_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/troubleshooting-ssh" | Str2html}} + {{ctx.Locale.Tr "settings.ssh_helper" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/connecting-to-github-with-ssh" "https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/troubleshooting-ssh"}} </p> </div> {{if .DisableSSH}} @@ -53,7 +53,7 @@ {{.Fingerprint}} </div> <div class="flex-item-body"> - <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i> + <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}} — {{svg "octicon-info"}} {{if .HasUsed}}{{ctx.Locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="text green"{{end}}>{{DateTime "short" .UpdatedUnix}}</span>{{else}}{{ctx.Locale.Tr "settings.no_activity"}}{{end}}</i> </div> </div> <div class="flex-item-trailing"> @@ -61,7 +61,7 @@ {{ctx.Locale.Tr "settings.delete_key"}} </button> {{if and (not .Verified) (ne $.VerifyingFingerprint .Fingerprint)}} - <a class="ui primary tiny button" href="{{$.Link}}?verify_ssh={{.Fingerprint}}">{{ctx.Locale.Tr "settings.ssh_key_verify"}}</a> + <a class="ui primary tiny button" href="?verify_ssh={{.Fingerprint}}">{{ctx.Locale.Tr "settings.ssh_key_verify"}}</a> {{end}} </div> </div> diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index a690d00352..c360944814 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -13,6 +13,9 @@ <a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{AppSubUrl}}/user/settings/security"> {{ctx.Locale.Tr "settings.security"}} </a> + <a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users"> + {{ctx.Locale.Tr "user.block.list"}} + </a> <a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{AppSubUrl}}/user/settings/applications"> {{ctx.Locale.Tr "settings.applications"}} </a> diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl index 8079521984..16c27b52cd 100644 --- a/templates/user/settings/organization.tmpl +++ b/templates/user/settings/organization.tmpl @@ -47,7 +47,7 @@ {{ctx.Locale.Tr "org.members.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.leave.detail" `<span class="dataOrganizationName"></span>` | Safe}}</p> + <p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/user/settings/packages.tmpl b/templates/user/settings/packages.tmpl index 1de20fe729..80853eab14 100644 --- a/templates/user/settings/packages.tmpl +++ b/templates/user/settings/packages.tmpl @@ -16,7 +16,7 @@ <button class="ui primary button">{{ctx.Locale.Tr "packages.owner.settings.chef.keypair"}}</button> </form> <div class="field"> - <label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/" | Safe}}</label> + <label>{{ctx.Locale.Tr "packages.registry.documentation" "Chef" "https://docs.gitea.com/usage/packages/chef/"}}</label> </div> </div> </div> diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 1f32aed0e8..aaaf8f30db 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -9,8 +9,8 @@ {{.CsrfTokenHtml}} <div class="required field {{if .Err_Name}}error{{end}}"> <label for="username">{{ctx.Locale.Tr "username"}} - <span class="text red gt-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span> - <span class="text red gt-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span> + <span class="text red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span> + <span class="text red tw-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span> </label> <input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" autofocus required {{if or (not .SignedUser.IsLocal) .IsReverseProxy}}disabled{{end}} maxlength="40"> {{if or (not .SignedUser.IsLocal) .IsReverseProxy}} @@ -22,8 +22,8 @@ <input id="full_name" name="full_name" value="{{.SignedUser.FullName}}" maxlength="100"> </div> <div class="field {{if .Err_Email}}error{{end}}"> - <label for="email">{{ctx.Locale.Tr "email"}}</label> - <p>{{.SignedUser.Email}}</p> + <label>{{ctx.Locale.Tr "email"}}</label> + <p id="signed-user-email">{{.SignedUser.Email}}</p> </div> <div class="field {{if .Err_Description}}error{{end}}"> <label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label> @@ -42,11 +42,11 @@ <!-- private block --> <div class="field" id="privacy-user-settings"> - <label for="security-private"><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label> + <label><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label> </div> <div class="inline field {{if .Err_Visibility}}error{{end}}"> - <span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span> + <span class="inline required field"><label>{{ctx.Locale.Tr "settings.visibility"}}</label></span> <div class="ui selection type dropdown"> {{if .SignedUser.Visibility.IsPublic}}<input type="hidden" id="visibility" name="visibility" value="0">{{end}} {{if .SignedUser.Visibility.IsLimited}}<input type="hidden" id="visibility" name="visibility" value="1">{{end}} @@ -106,7 +106,7 @@ <label>{{ctx.Locale.Tr "settings.lookup_avatar_by_mail"}}</label> </div> </div> - <div class="field gt-pl-4 {{if .Err_Gravatar}}error{{end}}"> + <div class="field tw-pl-4 {{if .Err_Gravatar}}error{{end}}"> <label for="gravatar">Avatar {{ctx.Locale.Tr "email"}}</label> <input id="gravatar" name="gravatar" value="{{.SignedUser.AvatarEmail}}"> </div> @@ -119,9 +119,9 @@ </div> </div> - <div class="inline field gt-pl-4"> - <label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label> - <input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp"> + <div class="inline field tw-pl-4"> + <label for="new-avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label> + <input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp"> </div> <div class="field"> diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl index 5aabec547a..26b9dfeed9 100644 --- a/templates/user/settings/repos.tmpl +++ b/templates/user/settings/repos.tmpl @@ -6,10 +6,10 @@ <div class="ui attached segment"> {{if or .allowAdopt .allowDelete}} {{if .Dirs}} - <div class="ui middle aligned divided list"> + <div class="ui list"> {{range $dirI, $dir := .Dirs}} {{$repo := index $.ReposMap $dir}} - <div class="item {{if not $repo}}gt-py-2{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}} + <div class="item {{if not $repo}}tw-py-1{{end}}">{{/* if not repo, then there are "adapt" buttons, so the padding shouldn't be that default large*/}} <div class="content"> {{if $repo}} {{if $repo.IsPrivate}} @@ -30,11 +30,11 @@ <span><a href="{{$repo.BaseRepo.Link}}">{{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}}</a></span> {{end}} {{else}} - <span class="icon gt-dib gt-pt-3">{{svg "octicon-file-directory-fill"}}</span> - <span class="name gt-dib gt-pt-3">{{$.ContextUser.Name}}/{{$dir}}</span> - <div class="gt-float-right"> + <span class="icon tw-inline-block tw-pt-2">{{svg "octicon-file-directory-fill"}}</span> + <span class="name tw-inline-block tw-pt-2">{{$.ContextUser.Name}}/{{$dir}}</span> + <div class="tw-float-right"> {{if $.allowAdopt}} - <button class="ui button primary show-modal gt-p-3" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button> + <button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button> <div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}"> <div class="header"> <span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span> @@ -51,7 +51,7 @@ </div> {{end}} {{if $.allowDelete}} - <button class="ui button red show-modal gt-p-3" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button> + <button class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button> <div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}"> <div class="header"> <span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span> @@ -86,15 +86,15 @@ <div class="item"> <div class="content"> {{if .IsPrivate}} - {{svg "octicon-lock" 16 "gt-mr-2 iconFloat text gold"}} + {{svg "octicon-lock" 16 "tw-mr-1 iconFloat text gold"}} {{else if .IsFork}} - {{svg "octicon-repo-forked" 16 "gt-mr-2 iconFloat"}} + {{svg "octicon-repo-forked" 16 "tw-mr-1 iconFloat"}} {{else if .IsMirror}} - {{svg "octicon-mirror" 16 "gt-mr-2 iconFloat"}} + {{svg "octicon-mirror" 16 "tw-mr-1 iconFloat"}} {{else if .IsTemplate}} - {{svg "octicon-repo-template" 16 "gt-mr-2 iconFloat"}} + {{svg "octicon-repo-template" 16 "tw-mr-1 iconFloat"}} {{else}} - {{svg "octicon-repo" 16 "gt-mr-2 iconFloat"}} + {{svg "octicon-repo" 16 "tw-mr-1 iconFloat"}} {{end}} <a class="name" href="{{.Link}}">{{.OwnerName}}/{{.Name}}</a> <span>{{FileSize .Size}}</span> diff --git a/templates/user/settings/security/openid.tmpl b/templates/user/settings/security/openid.tmpl index 0e9b4adcbe..b0473c9df5 100644 --- a/templates/user/settings/security/openid.tmpl +++ b/templates/user/settings/security/openid.tmpl @@ -7,7 +7,7 @@ {{ctx.Locale.Tr "settings.openid_desc"}} </div> {{range .OpenIDs}} - <div class="flex-item gt-ac"> + <div class="flex-item tw-items-center"> <div class="flex-item-leading"> {{svg "fontawesome-openid" 20}} </div> diff --git a/templates/user/settings/security/twofa.tmpl b/templates/user/settings/security/twofa.tmpl index 2f15fe13f1..adebce4265 100644 --- a/templates/user/settings/security/twofa.tmpl +++ b/templates/user/settings/security/twofa.tmpl @@ -4,7 +4,7 @@ <div class="ui attached segment"> <p>{{ctx.Locale.Tr "settings.twofa_desc"}}</p> {{if .TOTPEnrolled}} - <p>{{ctx.Locale.Tr "settings.twofa_is_enrolled" | Str2html}}</p> + <p>{{ctx.Locale.Tr "settings.twofa_is_enrolled"}}</p> <form class="ui form" action="{{AppSubUrl}}/user/settings/security/two_factor/regenerate_scratch" method="post" enctype="multipart/form-data"> {{.CsrfTokenHtml}} <p>{{ctx.Locale.Tr "settings.regenerate_scratch_token_desc"}}</p> diff --git a/templates/user/settings/security/webauthn.tmpl b/templates/user/settings/security/webauthn.tmpl index da6e5977c6..eceee191bd 100644 --- a/templates/user/settings/security/webauthn.tmpl +++ b/templates/user/settings/security/webauthn.tmpl @@ -1,6 +1,6 @@ <h4 class="ui top attached header">{{ctx.Locale.Tr "settings.webauthn"}}</h4> <div class="ui attached segment"> - <p>{{ctx.Locale.Tr "settings.webauthn_desc" | Str2html}}</p> + <p>{{ctx.Locale.Tr "settings.webauthn_desc"}}</p> <p>{{ctx.Locale.Tr "settings.webauthn_key_loss_warning"}} {{ctx.Locale.Tr "settings.webauthn_alternative_tip"}}</p> {{template "user/auth/webauthn_error" .}} <div class="flex-list"> @@ -12,7 +12,7 @@ <div class="flex-item-main"> <div class="flex-item-title">{{.Name}}</div> <div class="flex-item-body"> - <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix) | Safe}}</i> + <i>{{ctx.Locale.Tr "settings.added_on" (DateTime "short" .CreatedUnix)}}</i> </div> </div> <div class="flex-item-trailing"> diff --git a/templates/webhook/new.tmpl b/templates/webhook/new.tmpl index a185c42b51..8ef33df25b 100644 --- a/templates/webhook/new.tmpl +++ b/templates/webhook/new.tmpl @@ -1,7 +1,12 @@ <h4 class="ui top attached header"> {{.CustomHeaderTitle}} - <div class="ui right"> - {{template "shared/webhook/icon" .ctxData}} + <div class="ui right type dropdown"> + <div class="text tw-flex tw-items-center"> + {{template "shared/webhook/icon" (dict "Size" 20 "HookType" .ctxData.HookType)}} + {{ctx.Locale.Tr (print "repo.settings.web_hook_name_" .ctxData.HookType)}} + </div> + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + {{template "repo/settings/webhook/link_menu" .ctxData}} </div> </h4> <div class="ui attached segment"> diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index df4fe95fdb..d15aa9a027 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -75,7 +75,7 @@ func TestMain(m *testing.M) { // TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.js" files in this directory and build a test for each. func TestE2e(t *testing.T) { - // Find the paths of all e2e test files in test test directory. + // Find the paths of all e2e test files in test directory. searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js") paths, err := filepath.Glob(searchGlob) if err != nil { diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.js index c741663a38..57c69a2917 100644 --- a/tests/e2e/example.test.e2e.js +++ b/tests/e2e/example.test.e2e.js @@ -23,7 +23,7 @@ test('Test Register Form', async ({page}, workerInfo) => { await page.click('form button.ui.primary.button:visible'); // Make sure we routed to the home page. Else login failed. await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); - await expect(page.locator('.dashboard-navbar span>img.ui.avatar')).toBeVisible(); + await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible(); await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!'); save_visual(page); diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.js index fba13ab426..d60c78b16e 100644 --- a/tests/e2e/utils_e2e.js +++ b/tests/e2e/utils_e2e.js @@ -52,7 +52,7 @@ export async function save_visual(page) { fullPage: true, timeout: 20000, mask: [ - page.locator('.dashboard-navbar span>img.ui.avatar'), + page.locator('.secondary-nav span>img.ui.avatar'), page.locator('.ui.dropdown.jump.item span>img.ui.avatar'), ], }); diff --git a/tests/gitea-repositories-meta/org41/repo61.git/HEAD b/tests/gitea-repositories-meta/org41/repo61.git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/tests/gitea-repositories-meta/org41/repo61.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/org41/repo61.git/config b/tests/gitea-repositories-meta/org41/repo61.git/config new file mode 100644 index 0000000000..64280b806c --- /dev/null +++ b/tests/gitea-repositories-meta/org41/repo61.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = true + symlinks = false + ignorecase = true diff --git a/tests/gitea-repositories-meta/org41/repo61.git/description b/tests/gitea-repositories-meta/org41/repo61.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/org41/repo61.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/org41/repo61.git/info/exclude b/tests/gitea-repositories-meta/org41/repo61.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/tests/gitea-repositories-meta/org41/repo61.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7 new file mode 100644 index 0000000000..17a5547da8 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/81/a1c039774e337621609336c0e44ed9f92278f7 differ diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc new file mode 100644 index 0000000000..8390a40c08 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/91/dc55f9de16a558e859123f2b99668469b1a1dc differ diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02 new file mode 100644 index 0000000000..94312d3db6 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/a5/bbc0fd39a696feabed2d4cccaf05abbcaf3b02 @@ -0,0 +1 @@ +x��Kn� D��죱�v�EQ��~n�@3F���r�\,d��^�T��S�ϏGj|��K+D���� $2`�4��Y��Y�u{�Xho\���u4E;k- P4�Q^H�84��lk.��i�_��|g�V�v��=����|�-U�q8�;�ZJ��,��Nn�_����0�a�3���ҿ T�mķ�q�1m�؍b�^���z��/����_�5�zR'-'�~�tl� \ No newline at end of file diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a new file mode 100644 index 0000000000..b384e5c72e Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/cf/19952a40b92eb2f86689146a65ac2d87c0818a differ diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0 b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0 new file mode 100644 index 0000000000..da281ff791 Binary files /dev/null and b/tests/gitea-repositories-meta/user2/repo1.wiki.git/objects/e1/6da91326b845f1ba86a7df0a67db352f96dcb0 differ diff --git a/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master index 38984b12b7..b352f15003 100644 --- a/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master +++ b/tests/gitea-repositories-meta/user2/repo1.wiki.git/refs/heads/master @@ -1 +1 @@ -0dca5bd9b5d7ef937710e056f575e86c0184ba85 +a5bbc0fd39a696feabed2d4cccaf05abbcaf3b02 diff --git a/tests/gitea-repositories-meta/user40/repo60.git/HEAD b/tests/gitea-repositories-meta/user40/repo60.git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/tests/gitea-repositories-meta/user40/repo60.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/user40/repo60.git/config b/tests/gitea-repositories-meta/user40/repo60.git/config new file mode 100644 index 0000000000..64280b806c --- /dev/null +++ b/tests/gitea-repositories-meta/user40/repo60.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = true + symlinks = false + ignorecase = true diff --git a/tests/gitea-repositories-meta/user40/repo60.git/description b/tests/gitea-repositories-meta/user40/repo60.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/user40/repo60.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user40/repo60.git/info/exclude b/tests/gitea-repositories-meta/user40/repo60.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/tests/gitea-repositories-meta/user40/repo60.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 7744f33e57..2a2fdceb61 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -12,6 +12,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" @@ -19,8 +20,11 @@ import ( user_model "code.gitea.io/gitea/models/user" actions_module "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" pull_service "code.gitea.io/gitea/services/pull" + release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" @@ -199,6 +203,7 @@ func TestPullRequestTargetEvent(t *testing.T) { func TestSkipCI(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user2") user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // create the repo @@ -209,7 +214,7 @@ func TestSkipCI(t *testing.T) { Gitignores: "Go", License: "MIT", Readme: "Default", - DefaultBranch: "main", + DefaultBranch: "master", IsPrivate: false, }) assert.NoError(t, err) @@ -228,12 +233,12 @@ func TestSkipCI(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/pr.yml", - ContentReader: strings.NewReader("name: test\non:\n push:\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + ContentReader: strings.NewReader("name: test\non:\n push:\n branches: [master]\n pull_request:\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), }, }, Message: "add workflow", - OldBranch: "main", - NewBranch: "main", + OldBranch: "master", + NewBranch: "master", Author: &files_service.IdentityOptions{ Name: user2.Name, Email: user2.Email, @@ -263,8 +268,8 @@ func TestSkipCI(t *testing.T) { }, }, Message: fmt.Sprintf("%s add bar", setting.Actions.SkipWorkflowStrings[0]), - OldBranch: "main", - NewBranch: "main", + OldBranch: "master", + NewBranch: "master", Author: &files_service.IdentityOptions{ Name: user2.Name, Email: user2.Email, @@ -283,5 +288,158 @@ func TestSkipCI(t *testing.T) { // the commit message contains a configured skip-ci string, so there is still only 1 record assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID})) + + // add file to new branch + addFileToBranchResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "test-skip-ci", + ContentReader: strings.NewReader("test-skip-ci"), + }, + }, + Message: "add test file", + OldBranch: "master", + NewBranch: "test-skip-ci", + Author: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addFileToBranchResp) + + resp := testPullCreate(t, session, "user2", "skip-ci", true, "master", "test-skip-ci", "[skip ci] test-skip-ci") + + // check the redirected URL + url := test.RedirectURL(resp) + assert.Regexp(t, "^/user2/skip-ci/pulls/[0-9]*$", url) + + // the pr title contains a configured skip-ci string, so there is still only 1 record + assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID})) + }) +} + +func TestCreateDeleteRefEvent(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // create the repo + repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "create-delete-ref-event", + Description: "test create delete ref ci event", + AutoInit: true, + Gitignores: "Go", + License: "MIT", + Readme: "Default", + DefaultBranch: "main", + IsPrivate: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + // enable actions + err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypeActions, + }}, nil) + assert.NoError(t, err) + + // add workflow file to the repo + addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".gitea/workflows/createdelete.yml", + ContentReader: strings.NewReader("name: test\non:\n [create,delete]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"), + }, + }, + Message: "add workflow", + OldBranch: "main", + NewBranch: "main", + Author: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Committer: &files_service.IdentityOptions{ + Name: user2.Name, + Email: user2.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: time.Now(), + Committer: time.Now(), + }, + }) + assert.NoError(t, err) + assert.NotEmpty(t, addWorkflowToBaseResp) + + // Get the commit ID of the default branch + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch) + assert.NoError(t, err) + + // create a branch + err = repo_service.CreateNewBranchFromCommit(db.DefaultContext, user2, repo, gitRepo, branch.CommitID, "test-create-branch") + assert.NoError(t, err) + run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "create", + Ref: "refs/heads/test-create-branch", + WorkflowID: "createdelete.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + + // create a tag + err = release_service.CreateNewTag(db.DefaultContext, user2, repo, branch.CommitID, "test-create-tag", "test create tag event") + assert.NoError(t, err) + run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "create", + Ref: "refs/tags/test-create-tag", + WorkflowID: "createdelete.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + + // delete the branch + err = repo_service.DeleteBranch(db.DefaultContext, user2, repo, gitRepo, "test-create-branch") + assert.NoError(t, err) + run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "delete", + Ref: "main", + WorkflowID: "createdelete.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) + + // delete the tag + tag, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "test-create-tag") + assert.NoError(t, err) + err = release_service.DeleteReleaseByID(db.DefaultContext, repo, tag, user2, true) + assert.NoError(t, err) + run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ + Title: "add workflow", + RepoID: repo.ID, + Event: "delete", + Ref: "main", + WorkflowID: "createdelete.yml", + CommitSHA: branch.CommitID, + }) + assert.NotNil(t, run) }) } diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go new file mode 100644 index 0000000000..f58f876849 --- /dev/null +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -0,0 +1,224 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "io" + "net/http" + "strings" + "testing" + "time" + + "code.gitea.io/gitea/routers/api/actions" + actions_service "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func toProtoJSON(m protoreflect.ProtoMessage) io.Reader { + resp, _ := protojson.Marshal(m) + buf := bytes.Buffer{} + buf.Write(resp) + return &buf +} + +func TestActionsArtifactV4UploadSingleFile(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifact", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload url + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(body)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifact", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + +func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifact-invalid-checksum", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload url + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + + // upload artifact chunk + body := strings.Repeat("B", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(strings.Repeat("A", 1024))) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifact-invalid-checksum", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusInternalServerError) +} + +func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + ExpiresAt: timestamppb.New(time.Now().Add(5 * 24 * time.Hour)), + Name: "artifactWithRetentionDays", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload url + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(body)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifactWithRetentionDays", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + +func TestActionsArtifactV4DownloadSingle(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ + NameFilter: wrapperspb.String("artifact"), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var listResp actions.ListArtifactsResponse + protojson.Unmarshal(resp.Body.Bytes(), &listResp) + assert.Len(t, listResp.Artifacts, 1) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ + Name: "artifact", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.GetSignedArtifactURLResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.NotEmpty(t, finalizeResp.SignedUrl) + + req = NewRequest(t, "GET", finalizeResp.SignedUrl) + resp = MakeRequest(t, req, http.StatusOK) + body := strings.Repeat("A", 1024) + assert.Equal(t, resp.Body.String(), body) +} + +func TestActionsArtifactV4Delete(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // delete artifact by name + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/DeleteArtifact", toProtoJSON(&actions.DeleteArtifactRequest{ + Name: "artifact", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var deleteResp actions.DeleteArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &deleteResp) + assert.True(t, deleteResp.Ok) +} diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index 0748a75ba4..e8954f5b20 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -14,9 +14,11 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" + "github.com/gobwas/glob" "github.com/stretchr/testify/assert" ) @@ -333,3 +335,58 @@ func TestAPICron(t *testing.T) { } }) } + +func TestAPICreateUser_NotAllowedEmailDomain(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")} + defer func() { + setting.Service.EmailDomainAllowList = []glob.Glob{} + }() + + adminUsername := "user1" + token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin) + + req := NewRequestWithValues(t, "POST", "/api/v1/admin/users", map[string]string{ + "email": "allowedUser1@example1.org", + "login_name": "allowedUser1", + "username": "allowedUser1", + "password": "allowedUser1_pass", + "must_change_password": "true", + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + assert.Equal(t, "the domain of user email allowedUser1@example1.org conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning")) + + req = NewRequest(t, "DELETE", "/api/v1/admin/users/allowedUser1").AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) +} + +func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("example.org")} + defer func() { + setting.Service.EmailDomainAllowList = []glob.Glob{} + }() + + adminUsername := "user1" + token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin) + urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2") + + newEmail := "user2@example1.com" + req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ + LoginName: "user2", + SourceID: 0, + Email: &newEmail, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning")) + + originalEmail := "user2@example.com" + req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ + LoginName: "user2", + SourceID: 0, + Email: &originalEmail, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) +} diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go index a9c5228a16..255b8332b2 100644 --- a/tests/integration/api_comment_test.go +++ b/tests/integration/api_comment_test.go @@ -108,6 +108,32 @@ func TestAPICreateComment(t *testing.T) { DecodeJSON(t, resp, &updatedComment) assert.EqualValues(t, commentBody, updatedComment.Body) unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody}) + + t.Run("BlockedByRepoOwner", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{ + "body": commentBody, + }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository)) + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("BlockedByIssuePoster", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{ + "body": commentBody, + }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository)) + MakeRequest(t, req, http.StatusForbidden) + }) } func TestAPIGetComment(t *testing.T) { diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go index 4ca909f281..17e9f7aed5 100644 --- a/tests/integration/api_issue_reaction_test.go +++ b/tests/integration/api_issue_reaction_test.go @@ -58,6 +58,13 @@ func TestAPIIssuesReactions(t *testing.T) { // Add existing reaction MakeRequest(t, req, http.StatusForbidden) + // Blocked user can't react to comment + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) + req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{ + Reaction: "rocket", + }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue)) + MakeRequest(t, req, http.StatusForbidden) + // Get end result of reaction list of issue #1 req = NewRequest(t, "GET", urlStr). AddTokenAuth(token) diff --git a/tests/integration/api_issue_templates_test.go b/tests/integration/api_issue_templates_test.go new file mode 100644 index 0000000000..6b65e6e086 --- /dev/null +++ b/tests/integration/api_issue_templates_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestAPIIssueTemplateList(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + var issueTemplates []*api.IssueTemplate + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"}) + + // no issue template + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates") + resp := MakeRequest(t, req, http.StatusOK) + issueTemplates = nil + DecodeJSON(t, resp, &issueTemplates) + assert.Empty(t, issueTemplates) + + // one correct issue template and some incorrect issue templates + err := createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-ok.md", repo.DefaultBranch, `---- +name: foo +about: bar +---- +`) + assert.NoError(t, err) + + err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err1.yml", repo.DefaultBranch, `name: '`) + assert.NoError(t, err) + + err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err2.yml", repo.DefaultBranch, `other: `) + assert.NoError(t, err) + + req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates") + resp = MakeRequest(t, req, http.StatusOK) + issueTemplates = nil + DecodeJSON(t, resp, &issueTemplates) + assert.Len(t, issueTemplates, 1) + assert.Equal(t, "foo", issueTemplates[0].Name) + assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning")) + }) +} diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index f025806868..17b4e5bd71 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -84,7 +84,7 @@ func TestAPICreateIssue(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name) req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ Body: body, Title: title, @@ -106,6 +106,12 @@ func TestAPICreateIssue(t *testing.T) { repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues) assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues) + + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) + req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ + Title: title, + }).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue)) + MakeRequest(t, req, http.StatusForbidden) } func TestAPICreateIssueParallel(t *testing.T) { @@ -117,7 +123,7 @@ func TestAPICreateIssueParallel(t *testing.T) { session := loginUser(t, owner.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name) var wg sync.WaitGroup for i := 0; i < 10; i++ { @@ -217,7 +223,7 @@ func TestAPISearchIssues(t *testing.T) { defer tests.PrepareTestEnv(t)() // as this API was used in the frontend, it uses UI page size - expectedIssueCount := 18 // from the fixtures + expectedIssueCount := 20 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } @@ -257,7 +263,7 @@ func TestAPISearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count")) + assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 20) query.Add("limit", "10") @@ -265,7 +271,7 @@ func TestAPISearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count")) + assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 10) query = url.Values{"assigned": {"true"}, "state": {"all"}} @@ -315,7 +321,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { defer tests.PrepareTestEnv(t)() // as this API was used in the frontend, it uses UI page size - expectedIssueCount := 18 // from the fixtures + expectedIssueCount := 20 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } diff --git a/tests/integration/api_nodeinfo_test.go b/tests/integration/api_nodeinfo_test.go index 876fb5ac13..75f8dbb4ba 100644 --- a/tests/integration/api_nodeinfo_test.go +++ b/tests/integration/api_nodeinfo_test.go @@ -32,8 +32,8 @@ func TestNodeinfo(t *testing.T) { DecodeJSON(t, resp, &nodeinfo) assert.True(t, nodeinfo.OpenRegistrations) assert.Equal(t, "gitea", nodeinfo.Software.Name) - assert.Equal(t, 26, nodeinfo.Usage.Users.Total) - assert.Equal(t, 20, nodeinfo.Usage.LocalPosts) + assert.Equal(t, 29, nodeinfo.Usage.Users.Total) + assert.Equal(t, 22, nodeinfo.Usage.LocalPosts) assert.Equal(t, 3, nodeinfo.Usage.LocalComments) }) } diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index 1cd82fe4e0..70d3a446f7 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -177,7 +177,7 @@ func TestAPIGetAll(t *testing.T) { var apiOrgList []*api.Organization DecodeJSON(t, resp, &apiOrgList) - assert.Len(t, apiOrgList, 11) + assert.Len(t, apiOrgList, 12) assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName) assert.Equal(t, "limited", apiOrgList[1].Visibility) @@ -186,7 +186,7 @@ func TestAPIGetAll(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiOrgList) - assert.Len(t, apiOrgList, 7) + assert.Len(t, apiOrgList, 8) assert.Equal(t, "org 17", apiOrgList[0].FullName) assert.Equal(t, "public", apiOrgList[0].Visibility) } diff --git a/tests/integration/api_packages_alpine_test.go b/tests/integration/api_packages_alpine_test.go index 3cc7178e02..228f497127 100644 --- a/tests/integration/api_packages_alpine_test.go +++ b/tests/integration/api_packages_alpine_test.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" alpine_module "code.gitea.io/gitea/modules/packages/alpine" + alpine_service "code.gitea.io/gitea/services/packages/alpine" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -59,7 +60,34 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA` content, err := base64.StdEncoding.DecodeString(base64AlpinePackageContent) assert.NoError(t, err) - branches := []string{"v3.16", "v3.17", "v3.18"} + base64AlpinePackageNoArchContent := `H4sIAAAAAAACA9ML9nT30wsKdtQrLU4t0jUzTUo1NDVP0ysqTtQrKE1ioAYwAAIzExMwDQTotCGI +bWhiampuYmRiaGrMYGBoZGZkxKBgwEAHUFpcklikoMAwQkHLB7eoE40P9n5jvx32t7Dy9rq7x19k +66cJPV38t/h+vWe2jdXy+/PzPT0YTF5z39i4cPFptcLa1C1lD0z/XvrNp6In/7nP4PPCF2pZu8uV +z74QXLxpY1XWJuVFysqVf+PdizccFbD6ZL/QPGXd1Ri1fec2XBNuYfK/rFa6wF/h3dK/W12f8mxP +04iP3aCy+vPx7h9S+5M1LLkWr5M/4ezGt3bDW/FjBp/S9hiKP72s/XrJ0vWtO0zr5wa+D/X8XluW +d7BLP7XS3YUhd8WbPPF/NW3691ONJbXsRb69O7BIMZC96uTri+utC/fbie5J+n7zhCxD4Aep/qet +QnlCZyN8MhNdVNlNl7965R1nExrrGvfI/YQZFx8Dg+d9122hZsYd/24WL/L69OWrDAN/y//nS7im +XEive3v7QeTe433TPj/X71+9yHiV6+E9k++3TL8V0Xoq9panhNt23fLgau/pTOvmKx6bV/pS26+Y +5UP4viyuklYeu4/BZl6rLINe1L/uWuUXcH5z7pa2b9+/rp/v/8dFgc1PL3bO3/iVcrI//J/LMU2X +Nzu1IaMmWXnGp7CmyQIR39d0Nai9/+tdPbfjvmsNH88Tu7uVrvNuJE0wjxfePXGv/KHNXD+mnG0t +yTPu+Na0b5WR9O4t0yMd9T5k6ui7hOyU/jL/4dOn6neLwhdrZIZfcl1ectnGvUTurWDo1vY5Gw9k +PTQLVgcA61F+7gAEAAAfiwgAAAAAAAID7VVNa9wwEPXZv2Ig53hHlizbCzkVkobQJtDkB4wl2SvW +lhdbTpP++oyXQGEPLYU2paTvIs3X05PQSNnmjp4+OrJumjfZ3c3V9efL2+T3AhlaqePIOB0Rc50I +VRSlypUoZIJCKJQJPCVvgGWONLGU5H1CCDDRD+4CU57S6zT5j3eCP9Tyv9T/GsuT/scyLxPAt+z/ +aRzjj/J+Fv9HcQZXLriJorPQPAM1i+8tyEzkGZ5PmJ7BMvvQQUt7tx4BPPJH4ccAIpN5Jjj+hSJc +ugZAghDbArco4eH+A+SYq/Sw7wINDi6g89HReRhpMrvVzTzsFZlaV2Hbutmw4zVhmXo2djEe5u1m +c6zNzDikR3mW1a61JepaC0SZHsjsqTsyPoR9GL+GdPbf1iSFtU5Xyu/c4+Q7H04lMfvgI3vT3hsX +5rX40/U9b5CWOA78Mhrq+2ewLjrDp7VNWQbtaF6ZXVWZIhdV09RWOIvU6BqNboSxLSEpkrpQq80x +W1Nla6NavuqtrJQ0sv17D+4L2oD1lwAIAAAfiwgAAAAAAAID7dM/SgNBFAbw6cSAnYXlXsDNm50/ +u1METBeIkEBMK87uzKKEJbB/IN7CxhN4AI/gNcRD6BWciI0WSiBGxO/XvA9mile8L+5P7WrkrfN1 +049dV1XXbNso0FK+zeDzJC4SxqVSqUwkV4IR51KkLFqxHeia1tZhFfY/cR4V7VXlB9QL0b5HnUXD +6fj4bDI5ncXFpS8WTVfFs9GQD5wVxgrvlde5zMmJRKm89KVRmnhmyJYuo5RMj8Ef8EOV36j/6/yx +/5qnxKJ1J8MZJifskD2Zu+fzxfggmT+83F4c3dw/7u1vtf/1ctl+9e+7dwAAAAAAAAAAAAAAAAAA +AACAX/AKARNTyAAoAAA=` + noarchContent, err := base64.StdEncoding.DecodeString(base64AlpinePackageNoArchContent) + assert.NoError(t, err) + + branches := []string{"v3.16", "v3.17"} repositories := []string{"main", "testing"} rootURL := fmt.Sprintf("/api/packages/%s/alpine", user.Name) @@ -139,63 +167,71 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA` }) }) - t.Run("Index", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + readIndexContent := func(r io.Reader) (string, error) { + br := bufio.NewReader(r) - url := fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository) + gzr, err := gzip.NewReader(br) + if err != nil { + return "", err + } - req := NewRequest(t, "GET", url) - resp := MakeRequest(t, req, http.StatusOK) - - assert.Condition(t, func() bool { - br := bufio.NewReader(resp.Body) - - gzr, err := gzip.NewReader(br) - assert.NoError(t, err) + for { + gzr.Multistream(false) + tr := tar.NewReader(gzr) for { - gzr.Multistream(false) - - tr := tar.NewReader(gzr) - for { - hd, err := tr.Next() - if err == io.EOF { - break - } - assert.NoError(t, err) - - if hd.Name == "APKINDEX" { - buf, err := io.ReadAll(tr) - assert.NoError(t, err) - - s := string(buf) - - assert.Contains(t, s, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n") - assert.Contains(t, s, "P:"+packageName+"\n") - assert.Contains(t, s, "V:"+packageVersion+"\n") - assert.Contains(t, s, "A:x86_64\n") - assert.Contains(t, s, "T:Gitea Test Package\n") - assert.Contains(t, s, "U:https://gitea.io/\n") - assert.Contains(t, s, "L:MIT\n") - assert.Contains(t, s, "S:1353\n") - assert.Contains(t, s, "I:4096\n") - assert.Contains(t, s, "o:gitea-test\n") - assert.Contains(t, s, "m:KN4CK3R <kn4ck3r@gitea.io>\n") - assert.Contains(t, s, "t:1679498030\n") - - return true - } - } - - err = gzr.Reset(br) + hd, err := tr.Next() if err == io.EOF { break } - assert.NoError(t, err) + if err != nil { + return "", err + } + + if hd.Name == alpine_service.IndexFilename { + buf, err := io.ReadAll(tr) + if err != nil { + return "", err + } + + return string(buf), nil + } } - return false - }) + err = gzr.Reset(br) + if err == io.EOF { + break + } + if err != nil { + return "", err + } + } + + return "", io.EOF + } + + t.Run("Index", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + assert.Contains(t, content, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n") + assert.Contains(t, content, "P:"+packageName+"\n") + assert.Contains(t, content, "V:"+packageVersion+"\n") + assert.Contains(t, content, "A:x86_64\n") + assert.NotContains(t, content, "A:noarch\n") + assert.Contains(t, content, "T:Gitea Test Package\n") + assert.Contains(t, content, "U:https://gitea.io/\n") + assert.Contains(t, content, "L:MIT\n") + assert.Contains(t, content, "S:1353\n") + assert.Contains(t, content, "I:4096\n") + assert.Contains(t, content, "o:gitea-test\n") + assert.Contains(t, content, "m:KN4CK3R <kn4ck3r@gitea.io>\n") + assert.Contains(t, content, "t:1679498030\n") }) t.Run("Download", func(t *testing.T) { @@ -204,6 +240,35 @@ Djfa/2q5bH4699v++uMAAAAAAAAAAAAAAAAAAAAAAHbgA/eXQh8AKAAA` req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, packageName, packageVersion)) MakeRequest(t, req, http.StatusOK) }) + + t.Run("NoArch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s/%s", rootURL, branch, repository), bytes.NewReader(noarchContent)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)) + resp := MakeRequest(t, req, http.StatusOK) + + content, err := readIndexContent(resp.Body) + assert.NoError(t, err) + + assert.Contains(t, content, "C:Q1/se1PjO94hYXbfpNR1/61hVORIc=\n") + assert.Contains(t, content, "A:x86_64\n") + assert.Contains(t, content, "C:Q1kbH5WoIPFccQYyATanaKXd2cJcc=\n") + assert.NotContains(t, content, "A:noarch\n") + + // noarch package should be available with every architecture requested + for _, arch := range []string{alpine_module.NoArch, "x86_64", "my_arch"} { + req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s/gitea-noarch-1.4-r0.apk", rootURL, branch, repository, arch)) + MakeRequest(t, req, http.StatusOK) + } + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/noarch/gitea-noarch-1.4-r0.apk", rootURL, branch, repository)). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) }) } } diff --git a/tests/integration/api_packages_chef_test.go b/tests/integration/api_packages_chef_test.go index 4123c7216c..05545f11a6 100644 --- a/tests/integration/api_packages_chef_test.go +++ b/tests/integration/api_packages_chef_test.go @@ -11,6 +11,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha1" + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" @@ -33,7 +34,6 @@ import ( chef_router "code.gitea.io/gitea/routers/api/packages/chef" "code.gitea.io/gitea/tests" - "github.com/minio/sha256-simd" "github.com/stretchr/testify/assert" ) diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 509ad424e6..9ac6e5256b 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -5,6 +5,7 @@ package integration import ( "bytes" + "crypto/sha256" "encoding/base64" "fmt" "net/http" @@ -24,7 +25,6 @@ import ( "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" - "github.com/minio/sha256-simd" oci "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" ) diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go index 93525ac4b1..1cbae599af 100644 --- a/tests/integration/api_packages_generic_test.go +++ b/tests/integration/api_packages_generic_test.go @@ -84,7 +84,7 @@ func TestPackageGeneric(t *testing.T) { t.Run("InvalidParameter", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid+package name", packageVersion, filename), bytes.NewReader(content)). + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, "invalid package name", packageVersion, filename), bytes.NewReader(content)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusBadRequest) @@ -92,7 +92,7 @@ func TestPackageGeneric(t *testing.T) { AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusBadRequest) - req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inval+id.na me"), bytes.NewReader(content)). + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, "inva|id.name"), bytes.NewReader(content)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusBadRequest) }) diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go index 8c981566b6..daf32e82f9 100644 --- a/tests/integration/api_packages_test.go +++ b/tests/integration/api_packages_test.go @@ -5,6 +5,7 @@ package integration import ( "bytes" + "crypto/sha256" "fmt" "net/http" "strings" @@ -24,7 +25,6 @@ import ( packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup" "code.gitea.io/gitea/tests" - "github.com/minio/sha256-simd" "github.com/stretchr/testify/assert" ) diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go index daa136b21e..bc544a30b5 100644 --- a/tests/integration/api_pull_review_test.go +++ b/tests/integration/api_pull_review_test.go @@ -13,11 +13,14 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" + issue_service "code.gitea.io/gitea/services/issue" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "xorm.io/builder" ) func TestAPIPullReview(t *testing.T) { @@ -276,6 +279,49 @@ func TestAPIPullReviewRequest(t *testing.T) { }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) + // a collaborator can add/remove a review request + pullIssue21 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 21}) + assert.NoError(t, pullIssue21.LoadAttributes(db.DefaultContext)) + pull21Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue21.RepoID}) // repo60 + user38Session := loginUser(t, "user38") + user38Token := getTokenForLoggedInUser(t, user38Session, auth_model.AccessTokenScopeWriteRepository) + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{"user4@example.com"}, + }).AddTokenAuth(user38Token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{"user4@example.com"}, + }).AddTokenAuth(user38Token) + MakeRequest(t, req, http.StatusNoContent) + + // the poster of the PR can add/remove a review request + user39Session := loginUser(t, "user39") + user39Token := getTokenForLoggedInUser(t, user39Session, auth_model.AccessTokenScopeWriteRepository) + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{"user8"}, + }).AddTokenAuth(user39Token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull21Repo.OwnerName, pull21Repo.Name, pullIssue21.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{"user8"}, + }).AddTokenAuth(user39Token) + MakeRequest(t, req, http.StatusNoContent) + + // user with read permission on pull requests unit can add/remove a review request + pullIssue22 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 22}) + assert.NoError(t, pullIssue22.LoadAttributes(db.DefaultContext)) + pull22Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue22.RepoID}) // repo61 + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{"user38"}, + }).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit + MakeRequest(t, req, http.StatusCreated) + + req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", pull22Repo.OwnerName, pull22Repo.Name, pullIssue22.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{"user38"}, + }).AddTokenAuth(user39Token) // user39 is from a team with read permission on pull requests unit + MakeRequest(t, req, http.StatusNoContent) + // Test team review request pullIssue12 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12}) assert.NoError(t, pullIssue12.LoadAttributes(db.DefaultContext)) @@ -314,3 +360,126 @@ func TestAPIPullReviewRequest(t *testing.T) { AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) } + +func TestAPIPullReviewStayDismissed(t *testing.T) { + // This test against issue https://github.com/go-gitea/gitea/issues/28542 + // where old reviews surface after a review request got dismissed. + defer tests.PrepareTestEnv(t)() + pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + assert.NoError(t, pullIssue.LoadAttributes(db.DefaultContext)) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session2 := loginUser(t, user2.LoginName) + token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository) + user8 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 8}) + session8 := loginUser(t, user8.LoginName) + token8 := getTokenForLoggedInUser(t, session8, auth_model.AccessTokenScopeWriteRepository) + + // user2 request user8 + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{user8.LoginName}, + }).AddTokenAuth(token2) + MakeRequest(t, req, http.StatusCreated) + + reviewsCountCheck(t, + "check we have only one review request", + pullIssue.ID, user8.ID, 0, 1, 1, false) + + // user2 request user8 again, it is expected to be ignored + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{user8.LoginName}, + }).AddTokenAuth(token2) + MakeRequest(t, req, http.StatusCreated) + + reviewsCountCheck(t, + "check we have only one review request, even after re-request it again", + pullIssue.ID, user8.ID, 0, 1, 1, false) + + // user8 reviews it as accept + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + Event: "APPROVED", + Body: "lgtm", + }).AddTokenAuth(token8) + MakeRequest(t, req, http.StatusOK) + + reviewsCountCheck(t, + "check we have one valid approval", + pullIssue.ID, user8.ID, 0, 0, 1, true) + + // emulate of auto-dismiss lgtm on a protected branch that where a pull just got an update + _, err := db.GetEngine(db.DefaultContext).Where("issue_id = ? AND reviewer_id = ?", pullIssue.ID, user8.ID). + Cols("dismissed").Update(&issues_model.Review{Dismissed: true}) + assert.NoError(t, err) + + // user2 request user8 again + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers", repo.OwnerName, repo.Name, pullIssue.Index), &api.PullReviewRequestOptions{ + Reviewers: []string{user8.LoginName}, + }).AddTokenAuth(token2) + MakeRequest(t, req, http.StatusCreated) + + reviewsCountCheck(t, + "check we have no valid approval and one review request", + pullIssue.ID, user8.ID, 1, 1, 2, false) + + // user8 dismiss review + _, err = issue_service.ReviewRequest(db.DefaultContext, pullIssue, user8, user8, false) + assert.NoError(t, err) + + reviewsCountCheck(t, + "check new review request is now dismissed", + pullIssue.ID, user8.ID, 1, 0, 1, false) + + // add a new valid approval + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + Event: "APPROVED", + Body: "lgtm", + }).AddTokenAuth(token8) + MakeRequest(t, req, http.StatusOK) + + reviewsCountCheck(t, + "check that old reviews requests are deleted", + pullIssue.ID, user8.ID, 1, 0, 2, true) + + // now add a change request witch should dismiss the approval + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews", repo.OwnerName, repo.Name, pullIssue.Index), &api.CreatePullReviewOptions{ + Event: "REQUEST_CHANGES", + Body: "please change XYZ", + }).AddTokenAuth(token8) + MakeRequest(t, req, http.StatusOK) + + reviewsCountCheck(t, + "check that old reviews are dismissed", + pullIssue.ID, user8.ID, 2, 0, 3, false) +} + +func reviewsCountCheck(t *testing.T, name string, issueID, reviewerID int64, expectedDismissed, expectedRequested, expectedTotal int, expectApproval bool) { + t.Run(name, func(t *testing.T) { + unittest.AssertCountByCond(t, "review", builder.Eq{ + "issue_id": issueID, + "reviewer_id": reviewerID, + "dismissed": true, + }, expectedDismissed) + + unittest.AssertCountByCond(t, "review", builder.Eq{ + "issue_id": issueID, + "reviewer_id": reviewerID, + }, expectedTotal) + + unittest.AssertCountByCond(t, "review", builder.Eq{ + "issue_id": issueID, + "reviewer_id": reviewerID, + "type": issues_model.ReviewTypeRequest, + }, expectedRequested) + + approvalCount := 0 + if expectApproval { + approvalCount = 1 + } + unittest.AssertCountByCond(t, "review", builder.Eq{ + "issue_id": issueID, + "reviewer_id": reviewerID, + "type": issues_model.ReviewTypeApprove, + "dismissed": false, + }, approvalCount) + }) +} diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index 33cc826e5e..bb479caf89 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -61,6 +61,27 @@ func TestAPIViewPulls(t *testing.T) { } } +func TestAPIViewPullsByBaseHead(t *testing.T) { + defer tests.PrepareTestEnv(t)() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository) + + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch2", owner.Name, repo.Name). + AddTokenAuth(ctx.Token) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + + pull := &api.PullRequest{} + DecodeJSON(t, resp, pull) + assert.EqualValues(t, 3, pull.Index) + assert.EqualValues(t, 2, pull.ID) + + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls/master/branch-not-exist", owner.Name, repo.Name). + AddTokenAuth(ctx.Token) + ctx.Session.MakeRequest(t, req, http.StatusNotFound) +} + // TestAPIMergePullWIP ensures that we can't merge a WIP pull request func TestAPIMergePullWIP(t *testing.T) { defer tests.PrepareTestEnv(t)() @@ -105,6 +126,23 @@ func TestAPICreatePullSuccess(t *testing.T) { MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail } +func TestAPICreatePullSameRepoSuccess(t *testing.T) { + defer tests.PrepareTestEnv(t)() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner.Name, repo.Name), &api.CreatePullRequestOption{ + Head: fmt.Sprintf("%s:pr-to-update", owner.Name), + Base: "master", + Title: "successfully create a PR between branches of the same repository", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail +} + func TestAPICreatePullWithFieldsSuccess(t *testing.T) { defer tests.PrepareTestEnv(t)() // repo10 have code, pulls units. diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index 5b1ab76ce9..49aa4c4e1b 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -262,24 +262,60 @@ func TestAPIUploadAssetRelease(t *testing.T) { filename := "image.png" buff := generateImg() - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile("attachment", filename) - assert.NoError(t, err) - _, err = io.Copy(part, &buff) - assert.NoError(t, err) - err = writer.Close() - assert.NoError(t, err) + assetURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", owner.Name, repo.Name, r.ID) - req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset", owner.Name, repo.Name, r.ID), body). - AddTokenAuth(token) - req.Header.Add("Content-Type", writer.FormDataContentType()) - resp := MakeRequest(t, req, http.StatusCreated) + t.Run("multipart/form-data", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var attachment *api.Attachment - DecodeJSON(t, resp, &attachment) + body := &bytes.Buffer{} - assert.EqualValues(t, "test-asset", attachment.Name) - assert.EqualValues(t, 104, attachment.Size) + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("attachment", filename) + assert.NoError(t, err) + _, err = io.Copy(part, bytes.NewReader(buff.Bytes())) + assert.NoError(t, err) + err = writer.Close() + assert.NoError(t, err) + + req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())). + AddTokenAuth(token). + SetHeader("Content-Type", writer.FormDataContentType()) + resp := MakeRequest(t, req, http.StatusCreated) + + var attachment *api.Attachment + DecodeJSON(t, resp, &attachment) + + assert.EqualValues(t, filename, attachment.Name) + assert.EqualValues(t, 104, attachment.Size) + + req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=test-asset", bytes.NewReader(body.Bytes())). + AddTokenAuth(token). + SetHeader("Content-Type", writer.FormDataContentType()) + resp = MakeRequest(t, req, http.StatusCreated) + + var attachment2 *api.Attachment + DecodeJSON(t, resp, &attachment2) + + assert.EqualValues(t, "test-asset", attachment2.Name) + assert.EqualValues(t, 104, attachment2.Size) + }) + + t.Run("application/octet-stream", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(buff.Bytes())). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=stream.bin", bytes.NewReader(buff.Bytes())). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + + var attachment *api.Attachment + DecodeJSON(t, resp, &attachment) + + assert.EqualValues(t, "stream.bin", attachment.Name) + assert.EqualValues(t, 104, attachment.Size) + }) } diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go index 59cf85fef3..463db1dfb1 100644 --- a/tests/integration/api_repo_collaborator_test.go +++ b/tests/integration/api_repo_collaborator_test.go @@ -27,6 +27,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10}) user11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11}) + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository) @@ -86,6 +87,12 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) { MakeRequest(t, req, http.StatusNotFound) }) + t.Run("CollaboratorBlocked", func(t *testing.T) { + ctx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository) + ctx.ExpectedCode = http.StatusForbidden + doAPIAddCollaborator(ctx, user34.Name, perm.AccessModeAdmin)(t) + }) + t.Run("CollaboratorCanQueryItsPermissions", func(t *testing.T) { t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead)) diff --git a/tests/integration/api_repo_edit_test.go b/tests/integration/api_repo_edit_test.go index c4fc2177b4..7de8910ee0 100644 --- a/tests/integration/api_repo_edit_test.go +++ b/tests/integration/api_repo_edit_test.go @@ -65,6 +65,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption allowRebase := false allowRebaseMerge := false allowSquash := false + allowFastForwardOnly := false if unit, err := repo.GetUnit(db.DefaultContext, unit_model.TypePullRequests); err == nil { config := unit.PullRequestsConfig() hasPullRequests = true @@ -73,6 +74,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption allowRebase = config.AllowRebase allowRebaseMerge = config.AllowRebaseMerge allowSquash = config.AllowSquash + allowFastForwardOnly = config.AllowFastForwardOnly } archived := repo.IsArchived return &api.EditRepoOption{ @@ -92,6 +94,7 @@ func getRepoEditOptionFromRepo(repo *repo_model.Repository) *api.EditRepoOption AllowRebase: &allowRebase, AllowRebaseMerge: &allowRebaseMerge, AllowSquash: &allowSquash, + AllowFastForwardOnly: &allowFastForwardOnly, Archived: &archived, } } diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go index 0d192a1fe8..41ad7211ff 100644 --- a/tests/integration/api_repo_file_create_test.go +++ b/tests/integration/api_repo_file_create_test.go @@ -17,10 +17,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/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/context" "github.com/stretchr/testify/assert" ) diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 195a1090c7..ac28e0c0a2 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -16,10 +16,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/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/context" "github.com/stretchr/testify/assert" ) diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go index ab5cf19a9c..fb3ae5e4dd 100644 --- a/tests/integration/api_repo_files_change_test.go +++ b/tests/integration/api_repo_files_change_test.go @@ -15,10 +15,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/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/context" "github.com/stretchr/testify/assert" ) diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 90f84c794e..481732f8df 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -93,9 +93,9 @@ func TestAPISearchRepo(t *testing.T) { }{ { name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{ - nil: {count: 33}, - user: {count: 33}, - user2: {count: 33}, + nil: {count: 35}, + user: {count: 35}, + user2: {count: 35}, }, }, { diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go index c41bc4abb6..a10e159b78 100644 --- a/tests/integration/api_repo_topic_test.go +++ b/tests/integration/api_repo_topic_test.go @@ -26,14 +26,34 @@ func TestAPITopicSearch(t *testing.T) { TopicNames []*api.TopicResponse `json:"topics"` } + // search all topics + res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 6) + assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + + // pagination search topics first page + topics.TopicNames = nil query := url.Values{"page": []string{"1"}, "limit": []string{"4"}} searchURL.RawQuery = query.Encode() - res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) DecodeJSON(t, res, &topics) assert.Len(t, topics.TopicNames, 4) assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + // pagination search topics second page + topics.TopicNames = nil + query = url.Values{"page": []string{"2"}, "limit": []string{"4"}} + + searchURL.RawQuery = query.Encode() + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 2) + assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + + // add keyword search + query = url.Values{"page": []string{"1"}, "limit": []string{"4"}} query.Add("q", "topic") searchURL.RawQuery = query.Encode() res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go new file mode 100644 index 0000000000..7847962b07 --- /dev/null +++ b/tests/integration/api_repo_variables_test.go @@ -0,0 +1,149 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" +) + +func TestAPIRepoVariables(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + t.Run("CreateRepoVariable", func(t *testing.T) { + cases := []struct { + Name string + ExpectedStatus int + }{ + { + Name: "-", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "_", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "TEST_VAR", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "test_var", + ExpectedStatus: http.StatusConflict, + }, + { + Name: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "123var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "var@test", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "github_var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "gitea_var", + ExpectedStatus: http.StatusBadRequest, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.CreateVariableOption{ + Value: "value", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("UpdateRepoVariable", func(t *testing.T) { + variableName := "test_update_var" + url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName) + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + cases := []struct { + Name string + UpdateName string + ExpectedStatus int + }{ + { + Name: "not_found_var", + ExpectedStatus: http.StatusNotFound, + }, + { + Name: variableName, + UpdateName: "1invalid", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "invalid@name", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: variableName, + ExpectedStatus: http.StatusNotFound, + }, + { + Name: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.UpdateVariableOption{ + Name: c.UpdateName, + Value: "updated_val", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("DeleteRepoVariable", func(t *testing.T) { + variableName := "test_delete_var" + url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName) + + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + }) +} diff --git a/tests/integration/api_user_block_test.go b/tests/integration/api_user_block_test.go new file mode 100644 index 0000000000..2cc3895a71 --- /dev/null +++ b/tests/integration/api_user_block_test.go @@ -0,0 +1,243 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + "code.gitea.io/gitea/models" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestBlockUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + countStars := func(t *testing.T, repoOwnerID, starrerID int64) int64 { + count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.StarredReposOptions{ + StarrerID: starrerID, + RepoOwnerID: repoOwnerID, + IncludePrivate: true, + }) + assert.NoError(t, err) + return count + } + + countWatches := func(t *testing.T, repoOwnerID, watcherID int64) int64 { + count, err := db.Count[repo_model.Repository](db.DefaultContext, &repo_model.WatchedReposOptions{ + WatcherID: watcherID, + RepoOwnerID: repoOwnerID, + }) + assert.NoError(t, err) + return count + } + + countRepositoryTransfers := func(t *testing.T, senderID, recipientID int64) int64 { + transfers, err := models.GetPendingRepositoryTransfers(db.DefaultContext, &models.PendingRepositoryTransferOptions{ + SenderID: senderID, + RecipientID: recipientID, + }) + assert.NoError(t, err) + return int64(len(transfers)) + } + + countAssignedIssues := func(t *testing.T, repoOwnerID, assigneeID int64) int64 { + _, count, err := issues_model.GetAssignedIssues(db.DefaultContext, &issues_model.AssignedIssuesOptions{ + AssigneeID: assigneeID, + RepoOwnerID: repoOwnerID, + }) + assert.NoError(t, err) + return count + } + + countCollaborations := func(t *testing.T, repoOwnerID, collaboratorID int64) int64 { + count, err := db.Count[repo_model.Collaboration](db.DefaultContext, &repo_model.FindCollaborationOptions{ + CollaboratorID: collaboratorID, + RepoOwnerID: repoOwnerID, + }) + assert.NoError(t, err) + return count + } + + t.Run("User", func(t *testing.T) { + var blockerID int64 = 16 + blockerName := "user16" + blockerToken := getUserToken(t, blockerName, auth_model.AccessTokenScopeWriteUser) + + var blockeeID int64 = 10 + blockeeName := "user10" + + t.Run("Block", func(t *testing.T) { + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)) + MakeRequest(t, req, http.StatusUnauthorized) + + assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID)) + assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID)) + assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID)) + assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID)) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s?reason=test", blockeeName)). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusNoContent) + + assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID)) + assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID)) + assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID)) + assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID)) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user + + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusBadRequest) // can't block organization + + req = NewRequest(t, "GET", "/api/v1/user/blocks") + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "GET", "/api/v1/user/blocks"). + AddTokenAuth(blockerToken) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + + assert.Len(t, users, 1) + assert.Equal(t, blockeeName, users[0].UserName) + }) + + t.Run("Unblock", func(t *testing.T) { + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", blockeeName)). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", "org3")). + AddTokenAuth(blockerToken) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequest(t, "GET", "/api/v1/user/blocks"). + AddTokenAuth(blockerToken) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + + assert.Empty(t, users) + }) + }) + + t.Run("Organization", func(t *testing.T) { + var blockerID int64 = 3 + blockerName := "org3" + + doerToken := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteOrganization) + + var blockeeID int64 = 10 + blockeeName := "user10" + + t.Run("Block", func(t *testing.T) { + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "user4")). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusBadRequest) // can't block member + + assert.EqualValues(t, 1, countStars(t, blockerID, blockeeID)) + assert.EqualValues(t, 1, countWatches(t, blockerID, blockeeID)) + assert.EqualValues(t, 1, countRepositoryTransfers(t, blockerID, blockeeID)) + assert.EqualValues(t, 1, countAssignedIssues(t, blockerID, blockeeID)) + assert.EqualValues(t, 1, countCollaborations(t, blockerID, blockeeID)) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s?reason=test", blockerName, blockeeName)). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusNoContent) + + assert.EqualValues(t, 0, countStars(t, blockerID, blockeeID)) + assert.EqualValues(t, 0, countWatches(t, blockerID, blockeeID)) + assert.EqualValues(t, 0, countRepositoryTransfers(t, blockerID, blockeeID)) + assert.EqualValues(t, 0, countAssignedIssues(t, blockerID, blockeeID)) + assert.EqualValues(t, 0, countCollaborations(t, blockerID, blockeeID)) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusBadRequest) // can't block blocked user + + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusBadRequest) // can't block organization + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)). + AddTokenAuth(doerToken) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + + assert.Len(t, users, 1) + assert.Equal(t, blockeeName, users[0].UserName) + }) + + t.Run("Unblock", func(t *testing.T) { + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)) + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, blockeeName)). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/%s/blocks/%s", blockerName, "org3")). + AddTokenAuth(doerToken) + MakeRequest(t, req, http.StatusBadRequest) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/blocks", blockerName)). + AddTokenAuth(doerToken) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + + assert.Empty(t, users) + }) + }) +} diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go index 1762732c10..fe20af6769 100644 --- a/tests/integration/api_user_follow_test.go +++ b/tests/integration/api_user_follow_test.go @@ -9,6 +9,8 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" @@ -33,6 +35,12 @@ func TestAPIFollow(t *testing.T) { req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/following/%s", user1)). AddTokenAuth(token2) MakeRequest(t, req, http.StatusNoContent) + + // blocked user can't follow blocker + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) + req = NewRequest(t, "PUT", "/api/v1/user/following/user2"). + AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteUser)) + MakeRequest(t, req, http.StatusForbidden) }) t.Run("ListFollowing", func(t *testing.T) { diff --git a/tests/integration/api_user_star_test.go b/tests/integration/api_user_star_test.go index 50423c80e7..0062889a92 100644 --- a/tests/integration/api_user_star_test.go +++ b/tests/integration/api_user_star_test.go @@ -9,6 +9,8 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" @@ -31,6 +33,12 @@ func TestAPIStar(t *testing.T) { req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)). AddTokenAuth(tokenWithUserScope) MakeRequest(t, req, http.StatusNoContent) + + // blocked user can't star a repo + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)). + AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository)) + MakeRequest(t, req, http.StatusForbidden) }) t.Run("GetStarredRepos", func(t *testing.T) { diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go new file mode 100644 index 0000000000..dd5501f0b9 --- /dev/null +++ b/tests/integration/api_user_variables_test.go @@ -0,0 +1,144 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" +) + +func TestAPIUserVariables(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + + t.Run("CreateRepoVariable", func(t *testing.T) { + cases := []struct { + Name string + ExpectedStatus int + }{ + { + Name: "-", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "_", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "TEST_VAR", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "test_var", + ExpectedStatus: http.StatusConflict, + }, + { + Name: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "123var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "var@test", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "github_var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "gitea_var", + ExpectedStatus: http.StatusBadRequest, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.CreateVariableOption{ + Value: "value", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("UpdateRepoVariable", func(t *testing.T) { + variableName := "test_update_var" + url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName) + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + cases := []struct { + Name string + UpdateName string + ExpectedStatus int + }{ + { + Name: "not_found_var", + ExpectedStatus: http.StatusNotFound, + }, + { + Name: variableName, + UpdateName: "1invalid", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "invalid@name", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: variableName, + ExpectedStatus: http.StatusNotFound, + }, + { + Name: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.UpdateVariableOption{ + Name: c.UpdateName, + Value: "updated_val", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("DeleteRepoVariable", func(t *testing.T) { + variableName := "test_delete_var" + url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName) + + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + }) +} diff --git a/tests/integration/api_user_watch_test.go b/tests/integration/api_user_watch_test.go index 953e00551d..71dc57453e 100644 --- a/tests/integration/api_user_watch_test.go +++ b/tests/integration/api_user_watch_test.go @@ -9,6 +9,8 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" @@ -31,6 +33,12 @@ func TestAPIWatch(t *testing.T) { req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)). AddTokenAuth(tokenWithRepoScope) MakeRequest(t, req, http.StatusOK) + + // blocked user can't watch a repo + user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34}) + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription", repo)). + AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository)) + MakeRequest(t, req, http.StatusForbidden) }) t.Run("GetWatchedRepos", func(t *testing.T) { diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 1148b3ad39..0d733f663a 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -179,7 +179,7 @@ func TestLDAPUserSignin(t *testing.T) { assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name")) assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name")) - assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text()) + assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text()) } func TestLDAPAuthChange(t *testing.T) { @@ -309,7 +309,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { // all groups the user is a member of, the user filter is modified accordingly inside // the addAuthSourceLDAP based on the value of the groupFilter u := otherLDAPUsers[0] - testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect")) + testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect")) auth.SyncExternalUsers(context.Background(), true) @@ -362,7 +362,7 @@ func TestLDAPUserSigninFailed(t *testing.T) { addAuthSourceLDAP(t, "", "") u := otherLDAPUsers[0] - testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect")) + testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect")) } func TestLDAPUserSSHKeySync(t *testing.T) { @@ -428,9 +428,9 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) { isMember, err := organization.IsTeamMember(db.DefaultContext, usersOrgs[0].ID, team.ID, user.ID) assert.NoError(t, err) assert.True(t, isMember, "Membership should be added to the right team") - err = models.RemoveTeamMember(db.DefaultContext, team, user.ID) + err = models.RemoveTeamMember(db.DefaultContext, team, user) assert.NoError(t, err) - err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0].ID, user.ID) + err = models.RemoveOrgUser(db.DefaultContext, usersOrgs[0], user) assert.NoError(t, err) } else { // assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist @@ -460,7 +460,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { }) err = organization.AddOrgUser(db.DefaultContext, org.ID, user.ID) assert.NoError(t, err) - err = models.AddTeamMember(db.DefaultContext, team, user.ID) + err = models.AddTeamMember(db.DefaultContext, team, user) assert.NoError(t, err) isMember, err := organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID) assert.NoError(t, err) diff --git a/tests/integration/branches_test.go b/tests/integration/branches_test.go index 99d7eef706..e148fe2d6f 100644 --- a/tests/integration/branches_test.go +++ b/tests/integration/branches_test.go @@ -37,7 +37,7 @@ func TestUndoDeleteBranch(t *testing.T) { htmlDoc, name := branchAction(t, ".restore-branch-button") assert.Contains(t, htmlDoc.doc.Find(".ui.positive.message").Text(), - translation.NewLocale("en-US").Tr("repo.branch.restore_success", name), + translation.NewLocale("en-US").TrString("repo.branch.restore_success", name), ) }) } @@ -46,7 +46,7 @@ func deleteBranch(t *testing.T) { htmlDoc, name := branchAction(t, ".delete-branch-button") assert.Contains(t, htmlDoc.doc.Find(".ui.positive.message").Text(), - translation.NewLocale("en-US").Tr("repo.branch.deletion_success", name), + translation.NewLocale("en-US").TrString("repo.branch.deletion_success", name), ) } diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go index 468d13508d..75a4c1594f 100644 --- a/tests/integration/db_collation_test.go +++ b/tests/integration/db_collation_test.go @@ -22,12 +22,6 @@ type TestCollationTbl struct { func TestDatabaseCollation(t *testing.T) { x := db.GetEngine(db.DefaultContext).(*xorm.Engine) - // there are blockers for MSSQL to use case-sensitive collation, see the comments in db/collation.go - if setting.Database.Type.IsMSSQL() { - t.Skip("there are blockers for MSSQL to use case-sensitive collation") - return - } - // all created tables should use case-sensitive collation by default _, _ = x.Exec("DROP TABLE IF EXISTS test_collation_tbl") err := x.Sync(&TestCollationTbl{}) diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index de2a9d7d23..045567ce77 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -10,8 +10,8 @@ import ( "path" "testing" - gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" + gitea_context "code.gitea.io/gitea/services/context" "github.com/stretchr/testify/assert" ) diff --git a/tests/integration/explore_repos_test.go b/tests/integration/explore_repos_test.go index 26fd1dde64..1e3ab314fd 100644 --- a/tests/integration/explore_repos_test.go +++ b/tests/integration/explore_repos_test.go @@ -8,11 +8,18 @@ import ( "testing" "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" ) func TestExploreRepos(t *testing.T) { defer tests.PrepareTestEnv(t)() - req := NewRequest(t, "GET", "/explore/repos") - MakeRequest(t, req, http.StatusOK) + req := NewRequest(t, "GET", "/explore/repos?q=TheKeyword&topic=1&language=TheLang") + resp := MakeRequest(t, req, http.StatusOK) + respStr := resp.Body.String() + + assert.Contains(t, respStr, `<input type="hidden" name="topic" value="true">`) + assert.Contains(t, respStr, `<input type="hidden" name="language" value="TheLang">`) + assert.Contains(t, respStr, `<input type="search" name="q" value="TheKeyword"`) } diff --git a/tests/integration/explore_user_test.go b/tests/integration/explore_user_test.go new file mode 100644 index 0000000000..441d89cea5 --- /dev/null +++ b/tests/integration/explore_user_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestExploreUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + cases := []struct{ sortOrder, expected string }{ + {"", "?sort=newest&q="}, + {"newest", "?sort=newest&q="}, + {"oldest", "?sort=oldest&q="}, + {"alphabetically", "?sort=alphabetically&q="}, + {"reversealphabetically", "?sort=reversealphabetically&q="}, + } + for _, c := range cases { + req := NewRequest(t, "GET", "/explore/users?sort="+c.sortOrder) + resp := MakeRequest(t, req, http.StatusOK) + h := NewHTMLParser(t, resp.Body) + href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="?sort="]`).Attr("href") + assert.Equal(t, c.expected, href) + } + + // these sort orders shouldn't be supported, to avoid leaking user activity + cases404 := []string{ + "/explore/users?sort=lastlogin", + "/explore/users?sort=reverselastlogin", + "/explore/users?sort=leastupdate", + "/explore/users?sort=reverseleastupdate", + } + for _, c := range cases404 { + req := NewRequest(t, "GET", c).SetHeader("Accept", "text/html") + MakeRequest(t, req, http.StatusNotFound) + } +} diff --git a/tests/integration/git_clone_wiki_test.go b/tests/integration/git_clone_wiki_test.go index d7949dfe25..ef662300f3 100644 --- a/tests/integration/git_clone_wiki_test.go +++ b/tests/integration/git_clone_wiki_test.go @@ -45,6 +45,7 @@ func TestRepoCloneWiki(t *testing.T) { assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md")) assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md")) assertFileExist(t, filepath.Join(dstPath, "images")) + assertFileExist(t, filepath.Join(dstPath, "files/Non-Renderable-File.zip")) assertFileExist(t, filepath.Join(dstPath, "jpeg.jpg")) }) }) diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go new file mode 100644 index 0000000000..b37fb02444 --- /dev/null +++ b/tests/integration/git_push_test.go @@ -0,0 +1,159 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/url" + "testing" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + repo_service "code.gitea.io/gitea/services/repository" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGitPush(t *testing.T) { + onGiteaRun(t, testGitPush) +} + +func testGitPush(t *testing.T, u *url.URL) { + t.Run("Push branches at once", func(t *testing.T) { + runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { + for i := 0; i < 100; i++ { + branchName := fmt.Sprintf("branch-%d", i) + pushed = append(pushed, branchName) + doGitCreateBranch(gitPath, branchName)(t) + } + pushed = append(pushed, "master") + doGitPushTestRepository(gitPath, "origin", "--all")(t) + return pushed, deleted + }) + }) + + t.Run("Push branches one by one", func(t *testing.T) { + runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { + for i := 0; i < 100; i++ { + branchName := fmt.Sprintf("branch-%d", i) + doGitCreateBranch(gitPath, branchName)(t) + doGitPushTestRepository(gitPath, "origin", branchName)(t) + pushed = append(pushed, branchName) + } + return pushed, deleted + }) + }) + + t.Run("Push branch with options", func(t *testing.T) { + runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { + branchName := "branch-with-options" + doGitCreateBranch(gitPath, branchName)(t) + doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t) + pushed = append(pushed, branchName) + + return pushed, deleted + }) + }) + + t.Run("Delete branches", func(t *testing.T) { + runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { + doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete + pushed = append(pushed, "master") + + for i := 0; i < 100; i++ { + branchName := fmt.Sprintf("branch-%d", i) + pushed = append(pushed, branchName) + doGitCreateBranch(gitPath, branchName)(t) + } + doGitPushTestRepository(gitPath, "origin", "--all")(t) + + for i := 0; i < 10; i++ { + branchName := fmt.Sprintf("branch-%d", i) + doGitPushTestRepository(gitPath, "origin", "--delete", branchName)(t) + deleted = append(deleted, branchName) + } + return pushed, deleted + }) + }) + + t.Run("Push to deleted branch", func(t *testing.T) { + runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { + doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete + pushed = append(pushed, "master") + + doGitCreateBranch(gitPath, "branch-1")(t) + doGitPushTestRepository(gitPath, "origin", "branch-1")(t) + pushed = append(pushed, "branch-1") + + // delete and restore + doGitPushTestRepository(gitPath, "origin", "--delete", "branch-1")(t) + doGitPushTestRepository(gitPath, "origin", "branch-1")(t) + + return pushed, deleted + }) + }) +} + +func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gitPath string) (pushed, deleted []string)) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + Name: "repo-to-push", + Description: "test git push", + AutoInit: false, + DefaultBranch: "main", + IsPrivate: false, + }) + require.NoError(t, err) + require.NotEmpty(t, repo) + + gitPath := t.TempDir() + + doGitInitTestRepository(gitPath)(t) + + oldPath := u.Path + oldUser := u.User + defer func() { + u.Path = oldPath + u.User = oldUser + }() + u.Path = repo.FullName() + ".git" + u.User = url.UserPassword(user.LowerName, userPassword) + + doGitAddRemote(gitPath, "origin", u)(t) + + gitRepo, err := git.OpenRepository(git.DefaultContext, gitPath) + require.NoError(t, err) + defer gitRepo.Close() + + pushedBranches, deletedBranches := gitOperation(t, gitPath) + + dbBranches := make([]*git_model.Branch, 0) + require.NoError(t, db.GetEngine(db.DefaultContext).Where("repo_id=?", repo.ID).Find(&dbBranches)) + assert.Equalf(t, len(pushedBranches), len(dbBranches), "mismatched number of branches in db") + dbBranchesMap := make(map[string]*git_model.Branch, len(dbBranches)) + for _, branch := range dbBranches { + dbBranchesMap[branch.Name] = branch + } + + deletedBranchesMap := make(map[string]bool, len(deletedBranches)) + for _, branchName := range deletedBranches { + deletedBranchesMap[branchName] = true + } + + for _, branchName := range pushedBranches { + branch, ok := dbBranchesMap[branchName] + deleted := deletedBranchesMap[branchName] + assert.True(t, ok, "branch %s not found in database", branchName) + assert.Equal(t, deleted, branch.IsDeleted, "IsDeleted of %s is %v, but it's expected to be %v", branchName, branch.IsDeleted, deleted) + commitID, err := gitRepo.GetBranchCommitID(branchName) + require.NoError(t, err) + assert.Equal(t, commitID, branch.CommitID) + } + + require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID)) +} diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 0c3a8616f0..818e1fa653 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -4,6 +4,7 @@ package integration import ( + "bytes" "encoding/hex" "fmt" "math/rand" @@ -23,11 +24,13 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" 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/gitrepo" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + gitea_context "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -848,3 +851,44 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) } } + +func TestDataAsync_Issue29101(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "test.txt", + ContentReader: bytes.NewReader(make([]byte, 10000)), + }, + }, + OldBranch: repo.DefaultBranch, + NewBranch: repo.DefaultBranch, + }) + assert.NoError(t, err) + + sha := resp.Commit.SHA + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + assert.NoError(t, err) + + commit, err := gitRepo.GetCommit(sha) + assert.NoError(t, err) + + entry, err := commit.GetTreeEntryByPath("test.txt") + assert.NoError(t, err) + + b := entry.Blob() + + r, err := b.DataAsync() + assert.NoError(t, err) + defer r.Close() + + r2, err := b.DataAsync() + assert.NoError(t, err) + defer r2.Close() + }) +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 1127de1afc..f9bd352b62 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -24,7 +24,6 @@ import ( "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/unittest" - gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -33,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers" + gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 4b3f581c2b..44d362d9c7 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -407,7 +407,7 @@ func TestSearchIssues(t *testing.T) { session := loginUser(t, "user2") - expectedIssueCount := 18 // from the fixtures + expectedIssueCount := 20 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } @@ -444,7 +444,7 @@ func TestSearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count")) + assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 20) query.Add("limit", "5") @@ -452,7 +452,7 @@ func TestSearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.EqualValues(t, "20", resp.Header().Get("X-Total-Count")) + assert.EqualValues(t, "22", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 5) query = url.Values{"assigned": {"true"}, "state": {"all"}} @@ -501,7 +501,7 @@ func TestSearchIssues(t *testing.T) { func TestSearchIssuesWithLabels(t *testing.T) { defer tests.PrepareTestEnv(t)() - expectedIssueCount := 18 // from the fixtures + expectedIssueCount := 20 // from the fixtures if expectedIssueCount > setting.UI.IssuePagingNum { expectedIssueCount = setting.UI.IssuePagingNum } diff --git a/tests/integration/linguist_test.go b/tests/integration/linguist_test.go new file mode 100644 index 0000000000..e569de93a8 --- /dev/null +++ b/tests/integration/linguist_test.go @@ -0,0 +1,259 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "net/url" + "strings" + "testing" + "time" + + "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" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/indexer/stats" + "code.gitea.io/gitea/modules/queue" + repo_service "code.gitea.io/gitea/services/repository" + files_service "code.gitea.io/gitea/services/repository/files" + + "github.com/stretchr/testify/assert" +) + +func TestLinguist(t *testing.T) { + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + cppContent := "#include <iostream>\nint main() {\nstd::cout << \"Hello Gitea!\";\nreturn 0;\n}" + pyContent := "print(\"Hello Gitea!\")" + phpContent := "<?php\necho 'Hallo Welt';\n?>" + lockContent := "# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand." + mdContent := "markdown" + + cases := []struct { + GitAttributesContent string + FilesToAdd []*files_service.ChangeRepoFile + ExpectedLanguageOrder []string + }{ + // case 0 + { + ExpectedLanguageOrder: []string{}, + }, + // case 1 + { + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + { + TreePath: "python.py", + ContentReader: strings.NewReader(pyContent), + }, + { + TreePath: "php.php", + ContentReader: strings.NewReader(phpContent), + }, + }, + ExpectedLanguageOrder: []string{"C++", "PHP", "Python"}, + }, + // case 2 + { + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: ".cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + { + TreePath: "python.py", + ContentReader: strings.NewReader(pyContent), + }, + { + TreePath: "vendor/php.php", + ContentReader: strings.NewReader(phpContent), + }, + }, + ExpectedLanguageOrder: []string{"Python"}, + }, + // case 3 + { + GitAttributesContent: "*.cpp linguist-language=Go", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + }, + ExpectedLanguageOrder: []string{"Go"}, + }, + // case 4 + { + GitAttributesContent: "*.cpp gitlab-language=Go?parent=json", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + }, + ExpectedLanguageOrder: []string{"Go"}, + }, + // case 5 + { + GitAttributesContent: "*.cpp linguist-language=HTML gitlab-language=Go?parent=json", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + }, + ExpectedLanguageOrder: []string{"HTML"}, + }, + // case 6 + { + GitAttributesContent: "vendor/** linguist-vendored=false", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "vendor/php.php", + ContentReader: strings.NewReader(phpContent), + }, + }, + ExpectedLanguageOrder: []string{"PHP"}, + }, + // case 7 + { + GitAttributesContent: "*.cpp linguist-vendored=true\n*.py linguist-vendored\nvendor/** -linguist-vendored", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + { + TreePath: "python.py", + ContentReader: strings.NewReader(pyContent), + }, + { + TreePath: "vendor/php.php", + ContentReader: strings.NewReader(phpContent), + }, + }, + ExpectedLanguageOrder: []string{"PHP"}, + }, + // case 8 + { + GitAttributesContent: "poetry.lock linguist-language=Go", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "poetry.lock", + ContentReader: strings.NewReader(lockContent), + }, + }, + ExpectedLanguageOrder: []string{"Go"}, + }, + // case 9 + { + GitAttributesContent: "poetry.lock linguist-generated=false", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "poetry.lock", + ContentReader: strings.NewReader(lockContent), + }, + }, + ExpectedLanguageOrder: []string{"TOML"}, + }, + // case 10 + { + GitAttributesContent: "*.cpp -linguist-detectable", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + }, + ExpectedLanguageOrder: []string{}, + }, + // case 11 + { + GitAttributesContent: "*.md linguist-detectable", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "test.md", + ContentReader: strings.NewReader(mdContent), + }, + }, + ExpectedLanguageOrder: []string{"Markdown"}, + }, + // case 12 + { + GitAttributesContent: "test2.md linguist-detectable", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "cplusplus.cpp", + ContentReader: strings.NewReader(cppContent), + }, + { + TreePath: "test.md", + ContentReader: strings.NewReader(mdContent), + }, + { + TreePath: "test2.md", + ContentReader: strings.NewReader(mdContent), + }, + }, + ExpectedLanguageOrder: []string{"C++", "Markdown"}, + }, + // case 13 + { + GitAttributesContent: "README.md linguist-documentation=false", + FilesToAdd: []*files_service.ChangeRepoFile{ + { + TreePath: "README.md", + ContentReader: strings.NewReader(mdContent), + }, + }, + ExpectedLanguageOrder: []string{"Markdown"}, + }, + } + + for i, c := range cases { + repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + Name: "linguist-test", + }) + assert.NoError(t, err) + + files := []*files_service.ChangeRepoFile{ + { + TreePath: ".gitattributes", + ContentReader: strings.NewReader(c.GitAttributesContent), + }, + } + files = append(files, c.FilesToAdd...) + for _, f := range files { + f.Operation = "create" + } + + _, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{ + Files: files, + OldBranch: repo.DefaultBranch, + NewBranch: repo.DefaultBranch, + }) + assert.NoError(t, err) + + assert.NoError(t, stats.UpdateRepoIndexer(repo)) + assert.NoError(t, queue.GetManager().FlushAll(context.Background(), 10*time.Second)) + + stats, err := repo_model.GetTopLanguageStats(db.DefaultContext, repo, len(c.FilesToAdd)) + assert.NoError(t, err) + + languages := make([]string, 0, len(stats)) + for _, s := range stats { + languages = append(languages, s.Language) + } + assert.Equal(t, c.ExpectedLanguageOrder, languages, "case %d: unexpected language stats", i) + + assert.NoError(t, repo_service.DeleteRepository(db.DefaultContext, user, repo, false)) + } + }) +} diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go index a3937dd697..d103e2b0a9 100644 --- a/tests/integration/links_test.go +++ b/tests/integration/links_test.go @@ -36,6 +36,7 @@ func TestLinksNoLogin(t *testing.T) { "/user2/repo1/", "/user2/repo1/projects", "/user2/repo1/projects/1", + "/user2/repo1/releases/tag/delete-tag", // It's the only one existing record on release.yml which has is_tag: true "/assets/img/404.png", "/assets/img/500.png", "/.well-known/security.txt", diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index 2e71b80fbb..77050c4bbc 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/migration" - "code.gitea.io/gitea/modules/repository" mirror_service "code.gitea.io/gitea/services/mirror" release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" @@ -52,7 +51,7 @@ func TestMirrorPull(t *testing.T) { ctx := context.Background() - mirror, err := repository.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil) + mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil) assert.NoError(t, err) gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 3dc719593c..1c262b3349 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -15,10 +15,10 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" 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/gitrepo" "code.gitea.io/gitea/modules/setting" + gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" repo_service "code.gitea.io/gitea/services/repository" diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index f5baf05965..5ce8ea3031 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -25,4 +25,11 @@ func TestPullCompare(t *testing.T) { req = NewRequest(t, "GET", link) resp = session.MakeRequest(t, req, http.StatusOK) assert.EqualValues(t, http.StatusOK, resp.Code) + + // test the edit button in the PR diff view + req = NewRequest(t, "GET", "/user2/repo1/pulls/3/files") + resp = session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() + assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") } diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index fcd7fecd52..daf411f452 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -219,7 +219,7 @@ func TestCantMergeWorkInProgress(t *testing.T) { text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text()) assert.NotEmpty(t, text, "Can't find WIP text") - assert.Contains(t, text, translation.NewLocale("en-US").Tr("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text") + assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text") assert.Contains(t, text, "[wip]", "Unable to find WIP text") }) } @@ -365,6 +365,90 @@ func TestCantMergeUnrelated(t *testing.T) { }) } +func TestFastForwardOnlyMerge(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n") + + // Use API to create a pr from update to master + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{ + Head: "update", + Base: "master", + Title: "create a pr that can be fast-forward-only merged", + }).AddTokenAuth(token) + session.MakeRequest(t, req, http.StatusCreated) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ + Name: "user1", + }) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ + OwnerID: user1.ID, + Name: "repo1", + }) + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ + HeadRepoID: repo1.ID, + BaseRepoID: repo1.ID, + HeadBranch: "update", + BaseBranch: "master", + }) + + gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) + assert.NoError(t, err) + + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false) + + assert.NoError(t, err) + + gitRepo.Close() + }) +} + +func TestCantFastForwardOnlyMergeDiverging(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n") + testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n") + + // Use API to create a pr from diverging to update + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{ + Head: "diverging", + Base: "master", + Title: "create a pr from a diverging branch", + }).AddTokenAuth(token) + session.MakeRequest(t, req, http.StatusCreated) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ + Name: "user1", + }) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ + OwnerID: user1.ID, + Name: "repo1", + }) + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ + HeadRepoID: repo1.ID, + BaseRepoID: repo1.ID, + HeadBranch: "diverging", + BaseBranch: "master", + }) + + gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) + assert.NoError(t, err) + + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false) + + assert.Error(t, err, "Merge should return an error due to being for a diverging branch") + assert.True(t, models.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error") + + gitRepo.Close() + }) +} + func TestConflictChecking(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -432,8 +516,8 @@ func TestConflictChecking(t *testing.T) { assert.NoError(t, err) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"}) - conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID) - assert.NoError(t, err) + assert.NoError(t, issue.LoadPullRequest(db.DefaultContext)) + conflictingPR := issue.PullRequest // Ensure conflictedFiles is populated. assert.Len(t, conflictingPR.ConflictedFiles, 1) diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 68d80a1021..9a5877697c 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -5,9 +5,22 @@ package integration import ( "net/http" + "net/url" + "strings" "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" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/test" + issue_service "code.gitea.io/gitea/services/issue" + repo_service "code.gitea.io/gitea/services/repository" + files_service "code.gitea.io/gitea/services/repository/files" "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" ) func TestPullView_ReviewerMissed(t *testing.T) { @@ -15,8 +28,143 @@ func TestPullView_ReviewerMissed(t *testing.T) { session := loginUser(t, "user1") req := NewRequest(t, "GET", "/pulls") - session.MakeRequest(t, req, http.StatusOK) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) req = NewRequest(t, "GET", "/user2/repo1/pulls/3") - session.MakeRequest(t, req, http.StatusOK) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) + + // if some reviews are missing, the page shouldn't fail + err := db.TruncateBeans(db.DefaultContext, &issues_model.Review{}) + assert.NoError(t, err) + req = NewRequest(t, "GET", "/user2/repo1/pulls/2") + resp = session.MakeRequest(t, req, http.StatusOK) + assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) +} + +func TestPullView_CodeOwner(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // Create the repo. + repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ + Name: "test_codeowner", + Readme: "Default", + AutoInit: true, + ObjectFormatName: git.Sha1ObjectFormat.Name(), + DefaultBranch: "master", + }) + assert.NoError(t, err) + + // add CODEOWNERS to default branch + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + OldBranch: repo.DefaultBranch, + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "CODEOWNERS", + ContentReader: strings.NewReader("README.md @user5\n"), + }, + }, + }) + assert.NoError(t, err) + + t.Run("First Pull Request", func(t *testing.T) { + // create a new branch to prepare for pull request + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + NewBranch: "codeowner-basebranch", + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: "README.md", + ContentReader: strings.NewReader("# This is a new project\n"), + }, + }, + }) + assert.NoError(t, err) + + // Create a pull request. + session := loginUser(t, "user2") + testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request") + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "codeowner-basebranch"}) + unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) + assert.NoError(t, pr.LoadIssue(db.DefaultContext)) + + err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request") + assert.NoError(t, err) + prUpdated1 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) + assert.NoError(t, prUpdated1.LoadIssue(db.DefaultContext)) + assert.EqualValues(t, "[WIP] Test Pull Request", prUpdated1.Issue.Title) + + err = issue_service.ChangeTitle(db.DefaultContext, prUpdated1.Issue, user2, "Test Pull Request2") + assert.NoError(t, err) + prUpdated2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) + assert.NoError(t, prUpdated2.LoadIssue(db.DefaultContext)) + assert.EqualValues(t, "Test Pull Request2", prUpdated2.Issue.Title) + }) + + // change the default branch CODEOWNERS file to change README.md's codeowner + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: "CODEOWNERS", + ContentReader: strings.NewReader("README.md @user8\n"), + }, + }, + }) + assert.NoError(t, err) + + t.Run("Second Pull Request", func(t *testing.T) { + // create a new branch to prepare for pull request + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ + NewBranch: "codeowner-basebranch2", + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: "README.md", + ContentReader: strings.NewReader("# This is a new project2\n"), + }, + }, + }) + assert.NoError(t, err) + + // Create a pull request. + session := loginUser(t, "user2") + testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2") + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"}) + unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + }) + + t.Run("Forked Repo Pull Request", func(t *testing.T) { + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + forkedRepo, err := repo_service.ForkRepository(db.DefaultContext, user2, user5, repo_service.ForkRepoOptions{ + BaseRepo: repo, + Name: "test_codeowner", + }) + assert.NoError(t, err) + + // create a new branch to prepare for pull request + _, err = files_service.ChangeRepoFiles(db.DefaultContext, forkedRepo, user5, &files_service.ChangeRepoFilesOptions{ + NewBranch: "codeowner-basebranch-forked", + Files: []*files_service.ChangeRepoFile{ + { + Operation: "update", + TreePath: "README.md", + ContentReader: strings.NewReader("# This is a new forked project\n"), + }, + }, + }) + assert.NoError(t, err) + + session := loginUser(t, "user5") + testPullCreate(t, session, "user5", "test_codeowner", true, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2") + + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"}) + unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + }) + }) } diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 26c99e6445..bb7098e424 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -12,6 +12,9 @@ import ( "testing" 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/models/unittest" api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" @@ -90,6 +93,10 @@ func TestPullCreate_CommitStatus(t *testing.T) { assert.True(t, ok) assert.Contains(t, cls, statesIcons[status]) } + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) + css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID}) + assert.EqualValues(t, api.CommitStatusWarning, css.State) }) } diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go index 078253ffb0..5ae241f3af 100644 --- a/tests/integration/pull_update_test.go +++ b/tests/integration/pull_update_test.go @@ -177,8 +177,7 @@ func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_mod assert.NoError(t, err) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"}) - pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID) - assert.NoError(t, err) + assert.NoError(t, issue.LoadPullRequest(db.DefaultContext)) - return pr + return issue.PullRequest } diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index 42d0d00e78..ce0c440167 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -86,7 +86,7 @@ func TestCreateRelease(t *testing.T) { session := loginUser(t, "user2") createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4) + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.stable"), 4) } func TestCreateReleasePreRelease(t *testing.T) { @@ -95,7 +95,7 @@ func TestCreateReleasePreRelease(t *testing.T) { session := loginUser(t, "user2") createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.prerelease"), 4) + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.prerelease"), 4) } func TestCreateReleaseDraft(t *testing.T) { @@ -104,7 +104,7 @@ func TestCreateReleaseDraft(t *testing.T) { session := loginUser(t, "user2") createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.draft"), 4) + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.draft"), 4) } func TestCreateReleasePaging(t *testing.T) { @@ -124,11 +124,11 @@ func TestCreateReleasePaging(t *testing.T) { } createNewRelease(t, session, "/user2/repo1", "v0.0.12", "v0.0.12", false, true) - checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").Tr("repo.release.draft"), 10) + checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").TrString("repo.release.draft"), 10) // Check that user4 does not see draft and still see 10 latest releases session2 := loginUser(t, "user4") - checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").Tr("repo.release.stable"), 10) + checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").TrString("repo.release.stable"), 10) } func TestViewReleaseListNoLogin(t *testing.T) { @@ -234,7 +234,7 @@ func TestViewTagsList(t *testing.T) { tagNames := make([]string, 0, 5) tags.Each(func(i int, s *goquery.Selection) { - tagNames = append(tagNames, s.Find(".tag a.gt-df.gt-ac").Text()) + tagNames = append(tagNames, s.Find(".tag a.tw-flex.tw-items-center").Text()) }) assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames) diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go index 703fc243a4..13f6cf204b 100644 --- a/tests/integration/rename_branch_test.go +++ b/tests/integration/rename_branch_test.go @@ -5,17 +5,23 @@ package integration import ( "net/http" + "net/url" "testing" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) func TestRenameBranch(t *testing.T) { + onGiteaRun(t, testRenameBranch) +} + +func testRenameBranch(t *testing.T, u *url.URL) { defer tests.PrepareTestEnv(t)() unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: 1, Name: "master"}) @@ -26,20 +32,19 @@ func TestRenameBranch(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - postData := map[string]string{ + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{ "_csrf": htmlDoc.GetCSRF(), "from": "master", "to": "main", - } - req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", postData) + }) session.MakeRequest(t, req, http.StatusSeeOther) // check new branch link - req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", postData) + req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", nil) session.MakeRequest(t, req, http.StatusOK) // check old branch link - req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", postData) + req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", nil) resp = session.MakeRequest(t, req, http.StatusSeeOther) location := resp.Header().Get("Location") assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location) @@ -47,4 +52,69 @@ func TestRenameBranch(t *testing.T) { // check db repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) assert.Equal(t, "main", repo1.DefaultBranch) + + // create branch1 + csrf := GetCSRF(t, session, "/user2/repo1/src/branch/main") + + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{ + "_csrf": csrf, + "new_branch_name": "branch1", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + branch1 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.Equal(t, "branch1", branch1.Name) + + // create branch2 + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{ + "_csrf": csrf, + "new_branch_name": "branch2", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + branch2 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + assert.Equal(t, "branch2", branch2.Name) + + // rename branch2 to branch1 + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "from": "branch2", + "to": "branch1", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.Contains(t, flashCookie.Value, "error") + + branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + assert.Equal(t, "branch2", branch2.Name) + branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.Equal(t, "branch1", branch1.Name) + + // delete branch1 + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete", map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "name": "branch1", + }) + session.MakeRequest(t, req, http.StatusOK) + branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + assert.Equal(t, "branch2", branch2.Name) + branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.True(t, branch1.IsDeleted) // virtual deletion + + // rename branch2 to branch1 again + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "from": "branch2", + "to": "branch1", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + flashCookie = session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.Contains(t, flashCookie.Value, "success") + + unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.Equal(t, "branch1", branch1.Name) } diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 91674ddc82..baa8da4b75 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -52,37 +52,37 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) { OldRefSubURL: "branch/master", NewBranch: "feature/test1", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"), + FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test1"), }, { OldRefSubURL: "branch/master", NewBranch: "", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.require_error"), + FlashMessage: translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.require_error"), }, { OldRefSubURL: "branch/master", NewBranch: "feature=test1", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"), + FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature=test1"), }, { OldRefSubURL: "branch/master", NewBranch: strings.Repeat("b", 101), ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.max_size_error", "100"), + FlashMessage: translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.max_size_error", "100"), }, { OldRefSubURL: "branch/master", NewBranch: "master", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.branch_already_exists", "master"), + FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.branch_already_exists", "master"), }, { OldRefSubURL: "branch/master", NewBranch: "master/test", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.branch_name_conflict", "master/test", "master"), + FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.branch_name_conflict", "master/test", "master"), }, { OldRefSubURL: "commit/acd1d892867872cb47f3993468605b8aa59aa2e0", @@ -93,21 +93,21 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) { OldRefSubURL: "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d", NewBranch: "feature/test3", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"), + FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test3"), }, { OldRefSubURL: "branch/master", NewBranch: "v1.0.0", CreateRelease: "v1.0.0", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.tag_collision", "v1.0.0"), + FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.tag_collision", "v1.0.0"), }, { OldRefSubURL: "tag/v1.0.0", NewBranch: "feature/test4", CreateRelease: "v1.0.1", ExpectedStatus: http.StatusSeeOther, - FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"), + FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test4"), }, } for _, test := range tests { diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index 594fba6796..ca5d61ecc2 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -29,14 +29,14 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO // Step2: click the fork button htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href") + link, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href") assert.True(t, exists, "The template has changed") req = NewRequest(t, "GET", link) resp = session.MakeRequest(t, req, http.StatusOK) // Step3: fill the form of the forking htmlDoc = NewHTMLParser(t, resp.Body) - link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/fork/\"]").Attr("action") + link, exists = htmlDoc.doc.Find(`form.ui.form[action*="/fork"]`).Attr("action") assert.True(t, exists, "The template has changed") _, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value") assert.True(t, exists, fmt.Sprintf("Fork owner '%s' is not present in select box", forkOwnerName)) @@ -70,6 +70,6 @@ func TestRepoForkToOrg(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1") resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - _, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/fork/\"]").Attr("href") + _, exists := htmlDoc.doc.Find(`a.ui.button[href*="/fork"]`).Attr("href") assert.False(t, exists, "Forking should not be allowed anymore") } diff --git a/tests/integration/repo_search_test.go b/tests/integration/repo_search_test.go index cf199e98c2..56cc45d901 100644 --- a/tests/integration/repo_search_test.go +++ b/tests/integration/repo_search_test.go @@ -32,7 +32,7 @@ func TestSearchRepo(t *testing.T) { repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") assert.NoError(t, err) - executeIndexer(t, repo, code_indexer.UpdateRepoIndexer) + code_indexer.UpdateRepoIndexer(repo) testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"}) @@ -42,12 +42,14 @@ func TestSearchRepo(t *testing.T) { repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob") assert.NoError(t, err) - executeIndexer(t, repo, code_indexer.UpdateRepoIndexer) + code_indexer.UpdateRepoIndexer(repo) testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"}) - testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt"}) - testSearch(t, "/user2/glob/search?q=file4&page=1", []string{}) - testSearch(t, "/user2/glob/search?q=file5&page=1", []string{}) + testSearch(t, "/user2/glob/search?q=loren&page=1&t=match", []string{"a.txt"}) + testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt", "a.txt"}) + testSearch(t, "/user2/glob/search?q=file3&page=1&t=match", []string{"x/b.txt", "a.txt"}) + testSearch(t, "/user2/glob/search?q=file4&page=1&t=match", []string{"x/b.txt", "a.txt"}) + testSearch(t, "/user2/glob/search?q=file5&page=1&t=match", []string{"x/b.txt", "a.txt"}) } func testSearch(t *testing.T, url string, expected []string) { @@ -57,7 +59,3 @@ func testSearch(t *testing.T, url string, expected []string) { filenames := resultFilenames(t, NewHTMLParser(t, resp.Body)) assert.EqualValues(t, expected, filenames) } - -func executeIndexer(t *testing.T, repo *repo_model.Repository, op func(*repo_model.Repository)) { - op(repo) -} diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index f141b6dcb1..06c55b1e8a 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -77,7 +77,7 @@ func testViewRepo(t *testing.T) { }) // convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC" - htmlTimeString, _ := s.Find("relative-time.time-since").Attr("datetime") + htmlTimeString, _ := s.Find("relative-time").Attr("datetime") htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString) f.commitTime = htmlTime.In(time.Local).Format(time.RFC1123) items = append(items, f) diff --git a/tests/integration/repo_topic_test.go b/tests/integration/repo_topic_test.go index 58fee8418f..f198397007 100644 --- a/tests/integration/repo_topic_test.go +++ b/tests/integration/repo_topic_test.go @@ -21,20 +21,42 @@ func TestTopicSearch(t *testing.T) { TopicNames []*api.TopicResponse `json:"topics"` } + // search all topics + res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 6) + assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + + // pagination search topics + topics.TopicNames = nil query := url.Values{"page": []string{"1"}, "limit": []string{"4"}} searchURL.RawQuery = query.Encode() - res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) DecodeJSON(t, res, &topics) assert.Len(t, topics.TopicNames, 4) assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + // second page + topics.TopicNames = nil + query = url.Values{"page": []string{"2"}, "limit": []string{"4"}} + + searchURL.RawQuery = query.Encode() + res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) + DecodeJSON(t, res, &topics) + assert.Len(t, topics.TopicNames, 2) + assert.EqualValues(t, "6", res.Header().Get("x-total-count")) + + // add keyword search + topics.TopicNames = nil + query = url.Values{"page": []string{"1"}, "limit": []string{"4"}} query.Add("q", "topic") searchURL.RawQuery = query.Encode() res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) DecodeJSON(t, res, &topics) assert.Len(t, topics.TopicNames, 2) + topics.TopicNames = nil query.Set("q", "database") searchURL.RawQuery = query.Encode() res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go new file mode 100644 index 0000000000..ef44a9e2d0 --- /dev/null +++ b/tests/integration/repo_webhook_test.go @@ -0,0 +1,41 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "strings" + "testing" + + "code.gitea.io/gitea/tests" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" +) + +func TestNewWebHookLink(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + + baseurl := "/user2/repo1/settings/hooks" + tests := []string{ + // webhook list page + baseurl, + // new webhook page + baseurl + "/gitea/new", + // edit webhook page + baseurl + "/1", + } + + for _, url := range tests { + resp := session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + menus := htmlDoc.doc.Find(".ui.top.attached.header .ui.dropdown .menu a") + menus.Each(func(i int, menu *goquery.Selection) { + url, exist := menu.Attr("href") + assert.True(t, exist) + assert.True(t, strings.HasPrefix(url, baseurl)) + }) + } +} diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 19fbd1754c..49abeb83fb 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -12,11 +12,11 @@ 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/git" "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" files_service "code.gitea.io/gitea/services/repository/files" "github.com/stretchr/testify/assert" diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 2584b88f65..77e19bba96 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -49,10 +49,10 @@ func TestSignin(t *testing.T) { password string message string }{ - {username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, - {username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, - {username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, - {username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, + {username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")}, + {username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")}, + {username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")}, + {username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")}, } for _, s := range samples { diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index f983f98ad8..e9a05201ee 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -12,6 +12,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/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" @@ -58,7 +59,7 @@ func TestSignupAsRestricted(t *testing.T) { assert.True(t, user2.IsRestricted) } -func TestSignupEmail(t *testing.T) { +func TestSignupEmailValidation(t *testing.T) { defer tests.PrepareTestEnv(t)() setting.Service.EnableCaptcha = false @@ -68,9 +69,9 @@ func TestSignupEmail(t *testing.T) { wantStatus int wantMsg string }{ - {"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")}, - {"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")}, - {"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")}, + {"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")}, + {"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")}, + {"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")}, {"exampleUser@example.com", http.StatusSeeOther, ""}, } @@ -91,3 +92,62 @@ func TestSignupEmail(t *testing.T) { } } } + +func TestSignupEmailActive(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)() + + // try to sign up and send the activation email + req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ + "user_name": "test-user-1", + "email": "email-1@example.com", + "password": "password1", + "retype": "password1", + }) + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`) + + // access "user/activate" means trying to re-send the activation email + session := loginUserWithPassword(t, "test-user-1", "password1") + resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK) + assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently") + + // access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email + resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK) + assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `) + + // post to "user/activate" with a new email + session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) + assert.Equal(t, "email-changed@example.com", user.Email) + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"}) + assert.False(t, email.IsActivated) + assert.True(t, email.IsPrimary) + + // access "user/activate" with a valid activation code, then get the "verify password" page + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) + activationCode := user.GenerateEmailActivateCode(user.Email) + resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK) + assert.Contains(t, resp.Body.String(), `<input id="verify-password"`) + + // try to use a wrong password, it should fail + req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{ + "code": activationCode, + "password": "password-wrong", + }) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), `Your password does not match`) + assert.Contains(t, resp.Body.String(), `<input id="verify-password"`) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) + assert.False(t, user.IsActive) + + // then use a correct password, the user should be activated + req = NewRequestWithValues(t, "POST", "/user/activate", map[string]string{ + "code": activationCode, + "password": "password1", + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"}) + assert.True(t, user.IsActive) +} diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index d8e4c64e85..c4544f37aa 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -85,7 +85,7 @@ func TestRenameInvalidUsername(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) assert.Contains(t, htmlDoc.doc.Find(".ui.negative.message").Text(), - translation.NewLocale("en-US").Tr("form.username_error"), + translation.NewLocale("en-US").TrString("form.username_error"), ) unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername}) @@ -147,7 +147,7 @@ func TestRenameReservedUsername(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) assert.Contains(t, htmlDoc.doc.Find(".ui.negative.message").Text(), - translation.NewLocale("en-US").Tr("user.form.name_reserved", reservedUsername), + translation.NewLocale("en-US").TrString("user.form.name_reserved", reservedUsername), ) unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername}) @@ -243,6 +243,8 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) { } func TestGetUserRss(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user34 := "the_34-user.with.all.allowedChars" req := NewRequestf(t, "GET", "/%s.rss", user34) resp := MakeRequest(t, req, http.StatusOK) @@ -253,6 +255,13 @@ func TestGetUserRss(t *testing.T) { description, _ := rssDoc.ChildrenFiltered("description").Html() assert.EqualValues(t, "<p dir="auto">some <a href="https://commonmark.org/" rel="nofollow">commonmark</a>!</p>\n", description) } + + req = NewRequestf(t, "GET", "/non-existent-user.rss") + MakeRequest(t, req, http.StatusNotFound) + + session := loginUser(t, "user2") + req = NewRequestf(t, "GET", "/non-existent-user.rss") + session.MakeRequest(t, req, http.StatusNotFound) } func TestListStopWatches(t *testing.T) { diff --git a/build/generate-images.js b/tools/generate-images.js similarity index 75% rename from build/generate-images.js rename to tools/generate-images.js index a3a0f8d8f3..0bd3af29e4 100755 --- a/build/generate-images.js +++ b/tools/generate-images.js @@ -1,20 +1,13 @@ #!/usr/bin/env node -import imageminZopfli from 'imagemin-zopfli'; +import imageminZopfli from 'imagemin-zopfli'; // eslint-disable-line i/no-unresolved +import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line i/no-unresolved import {optimize} from 'svgo'; -import {fabric} from 'fabric'; import {readFile, writeFile} from 'node:fs/promises'; +import {argv, exit} from 'node:process'; -function exit(err) { +function doExit(err) { if (err) console.error(err); - process.exit(err ? 1 : 0); -} - -function loadSvg(svg) { - return new Promise((resolve) => { - fabric.loadSVGFromString(svg, (objects, options) => { - resolve({objects, options}); - }); - }); + exit(err ? 1 : 0); } async function generate(svg, path, {size, bg}) { @@ -27,7 +20,7 @@ async function generate(svg, path, {size, bg}) { 'removeDimensions', { name: 'addAttributesToSVGElement', - params: {attributes: [{width: size}, {height: size}]} + params: {attributes: [{width: size}, {height: size}]}, }, ], }); @@ -35,14 +28,14 @@ async function generate(svg, path, {size, bg}) { return; } - const {objects, options} = await loadSvg(svg); - const canvas = new fabric.Canvas(); + const {objects, options} = await loadSVGFromString(svg); + const canvas = new Canvas(); canvas.setDimensions({width: size, height: size}); const ctx = canvas.getContext('2d'); ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1); if (bg) { - canvas.add(new fabric.Rect({ + canvas.add(new Rect({ left: 0, top: 0, height: size * (1 / (size / options.height)), @@ -51,7 +44,7 @@ async function generate(svg, path, {size, bg}) { })); } - canvas.add(fabric.util.groupSVGElements(objects, options)); + canvas.add(util.groupSVGElements(objects, options)); canvas.renderAll(); let png = Buffer.from([]); @@ -64,7 +57,7 @@ async function generate(svg, path, {size, bg}) { } async function main() { - const gitea = process.argv.slice(2).includes('gitea'); + const gitea = argv.slice(2).includes('gitea'); const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8'); const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8'); @@ -79,4 +72,8 @@ async function main() { ]); } -main().then(exit).catch(exit); +try { + doExit(await main()); +} catch (err) { + doExit(err); +} diff --git a/build/generate-svg.js b/tools/generate-svg.js similarity index 92% rename from build/generate-svg.js rename to tools/generate-svg.js index b845da9367..f744162099 100755 --- a/build/generate-svg.js +++ b/tools/generate-svg.js @@ -4,15 +4,16 @@ import {optimize} from 'svgo'; import {parse} from 'node:path'; import {readFile, writeFile, mkdir} from 'node:fs/promises'; import {fileURLToPath} from 'node:url'; +import {exit} from 'node:process'; const glob = (pattern) => fastGlob.sync(pattern, { cwd: fileURLToPath(new URL('..', import.meta.url)), absolute: true, }); -function exit(err) { +function doExit(err) { if (err) console.error(err); - process.exit(err ? 1 : 0); + exit(err ? 1 : 0); } async function processFile(file, {prefix, fullName} = {}) { @@ -38,8 +39,8 @@ async function processFile(file, {prefix, fullName} = {}) { attributes: [ {'xmlns': 'http://www.w3.org/2000/svg'}, {'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'}, - ] - } + ], + }, }, ], }); @@ -63,4 +64,8 @@ async function main() { ]); } -main().then(exit).catch(exit); +try { + doExit(await main()); +} catch (err) { + doExit(err); +} diff --git a/tools/lint-templates-svg.js b/tools/lint-templates-svg.js new file mode 100755 index 0000000000..72f756400d --- /dev/null +++ b/tools/lint-templates-svg.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node +import {readdirSync, readFileSync} from 'node:fs'; +import {parse, relative} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {exit} from 'node:process'; +import fastGlob from 'fast-glob'; + +const knownSvgs = new Set(); +for (const file of readdirSync(new URL('../public/assets/img/svg', import.meta.url))) { + knownSvgs.add(parse(file).name); +} + +const rootPath = fileURLToPath(new URL('..', import.meta.url)); +let hadErrors = false; + +for (const file of fastGlob.sync(fileURLToPath(new URL('../templates/**/*.tmpl', import.meta.url)))) { + const content = readFileSync(file, 'utf8'); + for (const [_, name] of content.matchAll(/svg ["'`]([^"'`]+)["'`]/g)) { + if (!knownSvgs.has(name)) { + console.info(`SVG "${name}" not found, used in ${relative(rootPath, file)}`); + hadErrors = true; + } + } +} + +exit(hadErrors ? 1 : 0); diff --git a/build/watch.sh b/tools/watch.sh similarity index 100% rename from build/watch.sh rename to tools/watch.sh diff --git a/updates.config.js b/updates.config.js new file mode 100644 index 0000000000..11908dea8e --- /dev/null +++ b/updates.config.js @@ -0,0 +1,6 @@ +export default { + exclude: [ + '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled + 'eslint-plugin-array-func', // need to migrate to eslint flat config first + ], +}; diff --git a/vitest.config.js b/vitest.config.js index 9a6cb4e560..ea0fafeee8 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -5,8 +5,8 @@ import {stringPlugin} from 'vite-string-plugin'; export default defineConfig({ test: { include: ['web_src/**/*.test.js'], - setupFiles: ['./web_src/js/test/setup.js'], - environment: 'jsdom', + setupFiles: ['web_src/js/vitest.setup.js'], + environment: 'happy-dom', testTimeout: 20000, open: false, allowOnly: true, diff --git a/web_src/css/actions.css b/web_src/css/actions.css index e353a013a7..1d5bea2395 100644 --- a/web_src/css/actions.css +++ b/web_src/css/actions.css @@ -14,10 +14,6 @@ color: var(--color-red-light); } -.runner-container .runner-basic-info .gt-dib { - margin-right: 1em; -} - .runner-container .runner-new-text { color: var(--color-white); } @@ -48,7 +44,7 @@ } .run-list-item-right { - flex: 0 0 15%; + flex: 0 0 min(20%, 130px); display: flex; flex-direction: column; gap: 3px; diff --git a/web_src/css/base.css b/web_src/css/base.css index 198e87c0e2..c6a22a5dc4 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -21,13 +21,54 @@ --border-radius-circle: 50%; --opacity-disabled: 0.55; --height-loading: 16rem; + --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */ --tab-size: 4; + --checkbox-size: 15px; /* height and width of checkbox and radio inputs */ + --page-spacing: 16px; /* space between page elements */ + --page-margin-x: 32px; /* minimum space on left and right side of page */ +} + +@media (min-width: 768px) and (max-width: 1200px) { + :root { + --page-margin-x: 16px; + } +} + +@media (max-width: 767.98px) { + :root { + --page-margin-x: 8px; + } } :root * { --fonts-regular: var(--fonts-override, var(--fonts-proportional)), "Noto Sans", "Liberation Sans", sans-serif, var(--fonts-emoji); } +*, ::before, ::after { + /* these are needed for tailwind borders to work because we do not load tailwind's base + https://github.com/tailwindlabs/tailwindcss/blob/master/src/css/preflight.css */ + border-width: 0; + border-style: solid; + border-color: currentcolor; +} + +html, body { + height: 100%; + font-size: 14px; +} + +body { + line-height: 20px; + font-family: var(--fonts-regular); + color: var(--color-text); + background-color: var(--color-body); + tab-size: var(--tab-size); + display: flex; + flex-direction: column; + overflow-x: visible; + overflow-wrap: break-word; +} + textarea { font-family: var(--fonts-regular); } @@ -36,10 +77,17 @@ pre, code, kbd, samp { - font-size: 0.9em; /* compensate for monospace fonts being usually slightly larger */ font-family: var(--fonts-monospace); } +pre, +code, +kbd, +samp, +.tw-font-mono { + font-size: 0.95em; /* compensate for monospace fonts being usually slightly larger */ +} + b, strong, h1, @@ -51,13 +99,65 @@ h6 { font-weight: var(--font-weight-semibold); } -body { - color: var(--color-text); - background-color: var(--color-body); - tab-size: var(--tab-size); - display: flex; - flex-direction: column; - overflow-wrap: break-word; +h1, +h2, +h3, +h4, +h5 { + line-height: 1.28571429; + margin: calc(2rem - 0.1428571428571429em) 0 1rem; + font-weight: var(--font-weight-medium); + padding: 0; +} + +h1 { + min-height: 1rem; + font-size: 2rem; +} + +h2 { + font-size: 1.71428571rem; +} + +h3 { + font-size: 1.28571429rem; +} + +h4 { + font-size: 1.07142857rem; +} + +h5 { + font-size: 1rem; +} + +h1:first-child, +h2:first-child, +h3:first-child, +h4:first-child, +h5:first-child { + margin-top: 0; +} + +h1:last-child, +h2:last-child, +h3:last-child, +h4:last-child, +h5:last-child { + margin-bottom: 0; +} + +p { + margin: 0 0 1em; + line-height: 1.4285; +} + +p:first-child { + margin-top: 0; +} + +p:last-child { + margin-bottom: 0; } table { @@ -96,33 +196,9 @@ progress::-moz-progress-bar { } * { - scrollbar-color: var(--color-primary) transparent; caret-color: var(--color-caret); } -::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -::-webkit-scrollbar-thumb { - box-shadow: inset 0 0 0 6px var(--color-primary); - border: 2px solid transparent; - border-radius: var(--border-radius); -} - -::-webkit-scrollbar-thumb:window-inactive { - box-shadow: inset 0 0 0 6px var(--color-primary); -} - -::-webkit-scrollbar-thumb:hover { - box-shadow: inset 0 0 0 6px var(--color-primary-dark-2); -} - -::-webkit-scrollbar-corner { - background: transparent; -} - ::file-selector-button { border: 1px solid var(--color-light-border); color: var(--color-text-light); @@ -136,8 +212,8 @@ progress::-moz-progress-bar { } ::selection { - background: var(--color-primary-light-1) !important; - color: var(--color-white) !important; + background: var(--color-primary-light-1); + color: var(--color-white); } ::placeholder, @@ -161,28 +237,41 @@ progress::-moz-progress-bar { a { color: var(--color-primary); cursor: pointer; + text-decoration-line: none; text-decoration-skip-ink: all; } -/* muted link = only colored when hovered */ -/* silenced link = never colored */ +a:hover { + text-decoration-line: underline; +} + +/* a = always colored, underlined on hover */ +/* a.muted = colored on hover, underlined on hover */ +/* a.suppressed = never colored, underlined on hover */ +/* a.silenced = never colored, never underlined */ a.muted, +a.suppressed, a.silenced, .muted-links a { color: inherit; } a:hover, +a.suppressed:hover, a.muted:hover, a.muted:hover [class*="color-text"], .muted-links a:hover { color: var(--color-primary); } -a.silenced:hover { +a.silenced:hover, +a.suppressed:hover { color: inherit; - text-decoration: none; +} + +a.silenced:hover { + text-decoration-line: none; } a.label, @@ -190,7 +279,7 @@ a.label, .ui .menu a, .ui.cards a.card, .issue-keyword a { - text-decoration: none !important; + text-decoration-line: none !important; } .ui.search > .results { @@ -226,71 +315,19 @@ a.label, .inline-code-block { padding: 2px 4px; - border-radius: var(--border-radius-medium); - background-color: var(--color-markup-code-block); + border-radius: .24em; + background-color: var(--color-label-bg); } -.ui.dividing.header { - border-bottom-color: var(--color-secondary); -} - -.page-content { - margin-top: 15px; -} - -.page-content .header-wrapper, -.page-content .new-menu { - margin-top: -15px !important; - padding-top: 15px !important; -} - -/* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */ -.ui.input > input { - line-height: var(--line-height-default); - text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */ -} - -/* fix Fomantic's line-height causing vertical scrollbars to appear */ -ul.ui.list li, -ol.ui.list li, -.ui.list > .item, -.ui.list .list > .item { - line-height: var(--line-height-default); -} - -.ui.input.focus > input, -.ui.input > input:focus { - border-color: var(--color-primary); -} - -.ui.action.input .ui.ui.button { - border-color: var(--color-input-border); - padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */ - padding-bottom: 0; -} - -/* currently used for search bar dropdowns in repo search and explore code */ -.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection { - min-width: 10em; -} - -.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus,:hover) { - border-right-color: transparent; -} - -.ui.action.input:not([class*="left action"]) > input, -.ui.action.input:not([class*="left action"]) > input:hover { - border-right: 1px solid transparent; -} - -.ui.action.input:not([class*="left action"]) > input:focus { - border-right-color: var(--color-primary); +.ui.menu { + display: flex; } .ui.menu, .ui.vertical.menu { background: var(--color-menu); border-color: var(--color-secondary); + box-shadow: none; } .ui.menu .item { @@ -413,26 +450,18 @@ ol.ui.list li, color: var(--color-text-light-2); } +/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */ +.ui.dropdown > .text > .img { + margin-left: 0; + float: none; + margin-right: 0.78571429rem; +} + .ui.dropdown > .text > .description, .ui.dropdown .menu > .item > .description { color: var(--color-text-light-2); } -.ui.list .list > .item .header, -.ui.list > .item .header { - color: var(--color-text-dark); -} - -.ui.list .list > .item > .content, -.ui.list > .item > .content { - color: var(--color-text); -} - -.ui.list .list > .item .description, -.ui.list > .item .description { - color: var(--color-text); -} - /* replace item margin on secondary menu items with gap and remove both the negative margins on the menu as well as margin on the items */ .ui.secondary.menu { @@ -485,6 +514,11 @@ ol.ui.list li, background: var(--color-active) !important; } +.ui.form textarea:not([rows]) { + height: var(--min-height-textarea); /* override fomantic default 12em */ + min-height: var(--min-height-textarea); /* override fomantic default 8em */ +} + /* styles from removed fomantic transition module */ .hidden.transition { visibility: hidden; @@ -495,93 +529,6 @@ ol.ui.list li, visibility: visible !important; } -.ui.message { - background: var(--color-box-body); - color: var(--color-text); - box-shadow: none !important; - border: 1px solid var(--color-secondary); -} - -.ui.info.message .header, -.ui.blue.message .header { - color: var(--color-blue); -} - -.ui.info.message, -.ui.attached.info.message, -.ui.blue.message, -.ui.attached.blue.message { - background: var(--color-info-bg); - color: var(--color-info-text); - border-color: var(--color-info-border); -} - -.ui.success.message .header, -.ui.positive.message .header, -.ui.green.message .header { - color: var(--color-green); -} - -.ui.success.message, -.ui.attached.success.message, -.ui.positive.message, -.ui.attached.positive.message { - background: var(--color-success-bg); - color: var(--color-success-text); - border-color: var(--color-success-border); -} - -.ui.error.message .header, -.ui.negative.message .header, -.ui.red.message .header { - color: var(--color-red); -} - -.ui.error.message, -.ui.attached.error.message, -.ui.red.message, -.ui.attached.red.message, -.ui.negative.message, -.ui.attached.negative.message { - background: var(--color-error-bg); - color: var(--color-error-text); - border-color: var(--color-error-border); -} - -.ui.warning.message .header, -.ui.yellow.message .header { - color: var(--color-yellow); -} - -.ui.warning.message, -.ui.attached.warning.message, -.ui.yellow.message, -.ui.attached.yellow.message { - background: var(--color-warning-bg); - color: var(--color-warning-text); - border-color: var(--color-warning-border); -} - -.ui.error.header { - background: var(--color-error-bg) !important; - color: var(--color-error-text) !important; - border-color: var(--color-error-border) !important; -} - -.ui.error.segment { - border-color: var(--color-error-border) !important; -} - -.ui.warning.header { - background: var(--color-warning-bg) !important; - color: var(--color-warning-text) !important; - border-color: var(--color-warning-border) !important; -} - -.ui.warning.segment { - border-color: var(--color-warning-border) !important; -} - .ui.selection.active.dropdown, .ui.selection.active.dropdown:hover, .ui.selection.active.dropdown .menu, @@ -589,10 +536,6 @@ ol.ui.list li, border-color: var(--color-primary); } -.ui.selection.dropdown .menu { - margin: 0 -1.25px; -} - .ui.pointing.dropdown > .menu:not(.hidden)::after { background: var(--color-menu); box-shadow: -1px -1px 0 0 var(--color-secondary); @@ -629,86 +572,6 @@ ol.ui.list li, color: var(--color-primary); } -.ui.attached.table { - border-color: var(--color-secondary); -} - -.ui.table { - color: var(--color-text); - background: var(--color-box-body); - border-color: var(--color-secondary); - text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */ -} - -.ui.table th, -.ui.table td { - transition: none; -} - -.ui.table > tr > td, -.ui.table > tbody > tr > td { - border-top-color: var(--color-secondary-alpha-50); -} - -.ui.striped.table > tr:nth-child(2n), -.ui.striped.table > tbody > tr:nth-child(2n), -.ui.basic.striped.table > tbody > tr:nth-child(2n) { - background: var(--color-light); -} - -.ui.ui.ui.ui.table tr.active, -.ui.ui.table td.active { - color: var(--color-text); - background: var(--color-active); -} - -.ui.ui.selectable.table > tbody > tr:hover, -.ui.table tbody tr td.selectable:hover { - color: var(--color-text); - background-color: var(--color-secondary-alpha-40); -} - -.ui.ui.ui.ui.table tr.grey:not(.marked), -.ui.ui.table td.grey:not(.marked) { - background: var(--color-body); - color: var(--color-text); -} - -.ui.table > thead > tr > th { - background: var(--color-box-header); - border-color: var(--color-secondary); - color: var(--color-text); -} - -.ui.basic.table > tbody > tr { - border-color: var(--color-secondary); -} - -.ui.table > tfoot > tr > th, -.ui.table > tfoot > tr > td { - border-color: var(--color-secondary); - background: var(--color-box-body); - color: var(--color-text); -} - -/* reduce table padding, needed especially for dense admin tables */ -.ui.table > thead > tr > th, -.ui.table > tbody > tr > td, -.ui.table > tr > td { - padding: 6px 5px; -} -/* use more horizontal padding on first and last items for visuals */ -.ui.table > thead > tr > th:first-of-type, -.ui.table > tbody > tr > td:first-of-type, -.ui.table > tr > td:first-of-type { - padding-left: 10px; -} -.ui.table > thead > tr > th:last-of-type, -.ui.table > tbody > tr > td:last-of-type, -.ui.table > tr > td:last-of-type { - padding-right: 10px; -} - img.ui.avatar, .ui.avatar img, .ui.avatar svg { @@ -717,10 +580,6 @@ img.ui.avatar, aspect-ratio: 1; } -.ui.divided.list > .item { - border-color: var(--color-secondary); -} - .ui.error.message .header, .ui.warning.message .header { color: inherit; @@ -732,35 +591,19 @@ img.ui.avatar, padding-bottom: 80px; } -/* overwrite semantic width of containers inside the main page content div (div with class "page-content") */ -.page-content .ui.ui.ui.container:not(.fluid) { - width: 1280px; - max-width: calc(100% - 64px); - margin-left: auto; - margin-right: auto; +/* add margin below .secondary nav when it is the first child */ +.page-content > :first-child.secondary-nav { + margin-bottom: 14px; } -.ui.container.fluid.padded { - padding: 0 32px; +/* add margin to all pages when there is no .secondary.nav */ +.page-content > :first-child:not(.secondary-nav) { + margin-top: var(--page-spacing); } - -/* enable fluid page widths for medium size viewports */ -@media (min-width: 768px) and (max-width: 1200px) { - .page-content .ui.ui.ui.container:not(.fluid) { - max-width: calc(100% - 32px); - } - .ui.container.fluid.padded { - padding: 0 16px; - } -} - -@media (max-width: 767.98px) { - .page-content .ui.ui.ui.container:not(.fluid) { - max-width: calc(100% - 16px); - } - .ui.container.fluid.padded { - padding: 0 8px; - } +/* if .ui.grid is the first child the first grid-column has 'padding-top: 1rem' which we need + to compensate here */ +.page-content > :first-child.ui.grid { + margin-top: calc(var(--page-spacing) - 1rem); } .ui.pagination.menu .active.item { @@ -768,16 +611,6 @@ img.ui.avatar, background: var(--color-active); } -.ui.loading.segment::before, -.ui.loading.form::before { - background: none; -} - -.ui.loading.form > *, -.ui.loading.segment > * { - opacity: 0.35; -} - .ui.form .fields.error .field textarea, .ui.form .fields.error .field select, .ui.form .fields.error .field input:not([type]), @@ -851,20 +684,24 @@ img.ui.avatar, border-color: var(--color-error-border) !important; } -/* A fix for text visibility issue in Chrome autofill in dark mode. */ -/* It's a problem from Formatic UI, and this rule overrides it. */ -.ui.form .field.field input:-webkit-autofill { - -webkit-text-fill-color: var(--color-black) !important; +input:-webkit-autofill, +input:-webkit-autofill:focus, +input:-webkit-autofill:hover, +input:-webkit-autofill:active, +.ui.form .field.field input:-webkit-autofill, +.ui.form .field.field input:-webkit-autofill:focus, +.ui.form .field.field input:-webkit-autofill:hover, +.ui.form .field.field input:-webkit-autofill:active { + -webkit-background-clip: text; + -webkit-text-fill-color: var(--color-text); + box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important; + border-color: var(--color-primary-light-4) !important; } .ui.form .field.muted { opacity: var(--opacity-disabled); } -.ui.loading.loading.input > i.icon svg { - visibility: hidden; -} - .text.primary { color: var(--color-primary) !important; } @@ -949,20 +786,6 @@ img.ui.avatar, font-weight: var(--font-weight-normal); } -.ui.floating.label { - z-index: 10; -} - -.ui.transparent.label { - background-color: transparent; -} - -.ui.menu, -.ui.vertical.menu, -.ui.segment { - box-shadow: none; -} - /* replace fomantic popover box shadows */ .ui.dropdown .menu, .ui.upward.dropdown > .menu, @@ -997,14 +820,6 @@ img.ui.avatar, width: 100%; } -.ui.dropdown .menu > .item > .floating.label { - z-index: 11; -} - -.ui.dropdown .menu .menu > .item > .floating.label { - z-index: 21; -} - .ui.dropdown .menu > .header { font-size: 0.8em; } @@ -1056,23 +871,6 @@ img.ui.avatar, vertical-align: middle; } -.ui .info.segment.top h3, -.ui .info.segment.top h4 { - margin-top: 0; -} - -.ui .info.segment.top h3:last-child { - margin-top: 4px; -} - -.ui .info.segment.top > :last-child { - margin-bottom: 0; -} - -.ui .normal.header { - font-weight: var(--font-weight-normal); -} - .ui .form .autofill-dummy { position: absolute; width: 1px; @@ -1230,17 +1028,6 @@ img.ui.avatar, margin-right: 0; } -.ui.icon.header svg { - width: 3em; - height: 3em; - float: none; - display: block; - line-height: var(--line-height-default); - padding: 0; - margin: 0 auto 0.5rem; - opacity: 1; -} - .ui.floating.dropdown .overflow.menu .scrolling.menu.items { border-radius: 0 !important; box-shadow: none !important; @@ -1269,97 +1056,64 @@ img.ui.avatar, } .attention-icon { - vertical-align: text-top; + margin: 2px 6px 0 0; } -.attention-note { - font-weight: unset; - color: var(--color-info-text); +blockquote.attention-note { + border-left-color: var(--color-blue-dark-1); +} +strong.attention-note, svg.attention-note { + color: var(--color-blue-dark-1); } -.attention-warning { - font-weight: unset; +blockquote.attention-tip { + border-left-color: var(--color-success-text); +} +strong.attention-tip, svg.attention-tip { + color: var(--color-success-text); +} + +blockquote.attention-important { + border-left-color: var(--color-violet-dark-1); +} +strong.attention-important, svg.attention-important { + color: var(--color-violet-dark-1); +} + +blockquote.attention-warning { + border-left-color: var(--color-warning-text); +} +strong.attention-warning, svg.attention-warning { color: var(--color-warning-text); } +blockquote.attention-caution { + border-left-color: var(--color-red-dark-1); +} +strong.attention-caution, svg.attention-caution { + color: var(--color-red-dark-1); +} + .center:not(.popup) { text-align: center; } -@media (max-width: 767.98px) { - /* double selector so it wins over .gt-df etc */ - .not-mobile.not-mobile { - display: none !important; - } -} - -.ui.menu.new-menu { - margin-bottom: 15px; - background: var(--color-header-wrapper); +overflow-menu { border-bottom: 1px solid var(--color-secondary) !important; - overflow: auto; -} - -.ui.menu.new-menu .new-menu-inner { display: flex; - margin-left: auto; - margin-right: auto; - overflow-x: auto; - width: 100%; - mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 60px), transparent 100%); - -webkit-mask-image: linear-gradient(to right, #000 0%, #000 calc(100% - 60px), transparent 100%); } -.ui.menu.new-menu .item { +overflow-menu .overflow-menu-items { + display: flex; + flex: 1; +} + +overflow-menu .overflow-menu-items .item { margin-bottom: 0 !important; /* reset fomantic's margin, because the active menu has special bottom border */ } -@media (max-width: 767.98px) { - .ui.menu.new-menu .item { - width: auto !important; - } -} - -.ui.menu.new-menu .item:first-child { - margin-left: auto; /* "justify-content: center" doesn't work with "overflow: auto", so use margin: auto */ -} - -.ui.menu.new-menu .item:last-child { - padding-right: 30px !important; - margin-right: auto; -} - -.ui.menu.new-menu::-webkit-scrollbar { - height: 6px; - display: none; -} - -.ui.menu.new-menu::-webkit-scrollbar-track { - background: none !important; -} - -.ui.menu.new-menu::-webkit-scrollbar-thumb { - box-shadow: none !important; -} - -.ui.menu.new-menu:hover::-webkit-scrollbar { - display: block; -} - -.repos-search { - padding-bottom: 0 !important; -} - -.repos-filter { - margin-top: 0 !important; - border-bottom-width: 0 !important; - margin-bottom: 2px !important; - justify-content: space-evenly; -} - -.ui.secondary.pointing.menu.repos-filter .item { - padding-left: 4.5px; - padding-right: 4.5px; +overflow-menu .ui.label { + margin-left: 7px !important; /* save some space */ } .activity-bar-graph { @@ -1395,55 +1149,21 @@ img.ui.avatar, margin-top: 1px; } -.ui.label { - padding: 0.3em 0.5em; - transition: none; - white-space: nowrap; -} - -.ui.label, .ui.menu .item > .label { background: var(--color-label-bg); color: var(--color-label-text); } -.ui.label > a { - opacity: .75; /* increase contrast over default fomantic .5 */ -} - -.ui.active.label { - background: var(--color-label-active-bg); - border-color: var(--color-label-active-bg); - color: var(--color-label-text); -} - -.ui.labels a.label:hover, -a.ui.label:hover { - background: var(--color-label-hover-bg); - border-color: var(--color-label-hover-bg); - color: var(--color-label-text); -} - -.ui.labels a.active.label:hover, -a.ui.active.label:hover { - background: var(--color-label-active-bg); - border-color: var(--color-label-active-bg); - color: var(--color-label-text); -} - .lines-blame-btn { - padding-left: 10px; - padding-right: 10px; - text-align: right !important; - background-color: var(--color-code-sidebar-bg); - width: 2%; + padding: 0 0 0 5px; + display: flex; + justify-content: center; } .lines-num { - padding-left: 10px; - padding-right: 10px; + padding: 0 8px; text-align: right !important; - color: var(--color-text-light-1); + color: var(--color-text-light-2); width: 1%; font-family: var(--fonts-monospace); } @@ -1456,10 +1176,13 @@ a.ui.active.label:hover { content: attr(data-line-number); line-height: 20px !important; padding: 0 10px; - cursor: pointer; display: block; } +.code-view .lines-num span::after { + cursor: pointer; +} + .lines-type-marker { vertical-align: top; } @@ -1497,22 +1220,34 @@ a.ui.active.label:hover { } .lines-code { - background-color: var(--color-code-bg); padding-left: 5px; } -.lines-code.active, -.lines-code .active { - background: var(--color-active-line) !important; +.file-view tr.active { + color: inherit !important; + background: inherit !important; } -.blame .lines-num { - padding: 0 !important; - background-color: var(--color-code-sidebar-bg); +.file-view tr.active .lines-num, +.file-view tr.active .lines-code { + background: var(--color-highlight-bg) !important; } -.blame .lines-code { - padding: 0 !important; +.file-view tr.active:last-of-type .lines-code { + border-bottom-right-radius: var(--border-radius); +} + +.file-view tr.active .lines-num { + position: relative; +} + +.file-view tr.active .lines-num::before { + content: ""; + position: absolute; + left: 0; + width: 2px; + height: 100%; + background: var(--color-highlight-fg); } .code-inner { @@ -1520,27 +1255,25 @@ a.ui.active.label:hover { white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere; + line-height: inherit; /* needed for inline code preview in markup */ } .blame .code-inner { - white-space: pre; - word-break: normal; - word-wrap: normal; /* not using overflow-wrap because safari does not treat is an an alias */ + white-space: pre-wrap; + overflow-wrap: anywhere; } .lines-commit { vertical-align: top; - color: var(--color-grey); + color: var(--color-text-light-1); padding: 0 !important; - background: var(--color-code-sidebar-bg); width: 1%; } .lines-commit .blame-info { - width: 350px; - max-width: 350px; + width: min(26vw, 300px); display: block; - padding: 0 0 0 10px; + padding: 0 0 0 6px; line-height: 20px; box-sizing: content-box; } @@ -1562,166 +1295,34 @@ a.ui.active.label:hover { flex-shrink: 0; } -.lines-commit .ui.avatar { - height: 18px; - width: 18px; - display: block; - margin-top: 1px; +.blame-avatar { + display: flex; + align-items: center; + margin-right: 4px; } .top-line-blame { border-top: 1px solid var(--color-secondary); } +.code-view tr.top-line-blame:first-of-type { + border-top: none; +} + .lines-code .bottom-line, .lines-commit .bottom-line { border-bottom: 1px solid var(--color-secondary); } +.code-view { + background: var(--color-code-bg); + border-radius: var(--border-radius); +} + .code-view table { width: 100%; } -.ui.primary.label, -.ui.primary.labels .label, -.ui.ui.ui.primary.label { - background-color: var(--color-primary); - border-color: var(--color-primary-dark-2); -} - -.ui.basic.labels .primary.label, -.ui.ui.ui.basic.primary.label { - background: transparent; - border-color: var(--color-primary); - color: var(--color-primary); -} - -.ui.basic.labels a.primary.label:hover, -a.ui.ui.ui.basic.primary.label:hover { - background: var(--color-hover); - border-color: var(--color-primary-dark-1); - color: var(--color-primary-dark-1); -} - -.ui.basic.labels .secondary.label, -.ui.ui.ui.basic.secondary.label { - background: transparent; - border-color: var(--color-secondary); - color: var(--color-secondary); -} - -.ui.basic.labels .orange.label, -.ui.ui.ui.basic.orange.label { - background: transparent; - border-color: var(--color-orange); - color: var(--color-orange); -} - -.ui.basic.labels .green.label, -.ui.ui.ui.basic.green.label { - background: transparent; - border-color: var(--color-green); - color: var(--color-green); -} - -.ui.basic.labels .olive.label, -.ui.ui.ui.basic.olive.label { - background: transparent; - border-color: var(--color-olive); - color: var(--color-olive); -} - -.ui.basic.labels .teal.label, -.ui.ui.ui.basic.teal.label { - background: transparent; - border-color: var(--color-teal); - color: var(--color-teal); -} - -.ui.basic.labels .blue.label, -.ui.ui.ui.basic.blue.label { - background: transparent; - border-color: var(--color-blue); - color: var(--color-blue); -} - -.ui.basic.labels .violet.label, -.ui.ui.ui.basic.violet.label { - background: transparent; - border-color: var(--color-violet); - color: var(--color-violet); -} - -.ui.basic.labels .purple.label, -.ui.ui.ui.basic.purple.label { - background: transparent; - border-color: var(--color-purple); - color: var(--color-purple); -} - -.ui.basic.labels .pink.label, -.ui.ui.ui.basic.pink.label { - background: transparent; - border-color: var(--color-pink); - color: var(--color-pink); -} - -.ui.basic.labels .red.label, -.ui.ui.ui.basic.red.label { - background: transparent; - border-color: var(--color-red); - color: var(--color-red); -} - -.ui.basic.labels .brown.label, -.ui.ui.ui.basic.brown.label { - background: transparent; - border-color: var(--color-brown); - color: var(--color-brown); -} - -.ui.basic.labels .yellow.label, -.ui.ui.ui.basic.yellow.label { - background: transparent; - border-color: var(--color-yellow); - color: var(--color-yellow); -} - -.ui.basic.labels .grey.label, -.ui.ui.ui.basic.grey.label { - background: transparent; - border-color: var(--color-grey); - color: var(--color-grey); -} - -.ui.basic.labels .black.label, -.ui.ui.ui.basic.black.label { - background: transparent; - border-color: var(--color-black); - color: var(--color-black); -} - -.ui.basic.labels .label, -.ui.basic.label, -.ui.secondary.labels .ui.basic.label { - background: var(--color-button); - border-color: var(--color-light-border); - color: var(--color-text-light); -} - -.ui.basic.labels a.label:hover, -a.ui.basic.label:hover { - color: var(--color-text); - border-color: var(--color-light-border); - background: var(--color-hover); -} - -.ui.label > img { - width: auto !important; - vertical-align: middle; - height: 2.1666em !important; -} - .migrate .svg.gitea-git { color: var(--color-git); } @@ -1733,56 +1334,6 @@ a.ui.basic.label:hover { width: 14px; } -.ui.label > .color-icon { - margin-left: 0; -} - -.ui.segment, -.ui.segments, -.ui.attached.segment { - background: var(--color-box-body); - color: var(--color-text); - border-color: var(--color-secondary); -} - -.ui.segments > .segment { - border-color: var(--color-secondary); -} - -.ui.secondary.segment { - background: var(--color-secondary-bg); - color: var(--color-text-light); -} - -.ui.attached.header { - position: relative; - background: var(--color-box-header); - border-color: var(--color-secondary); -} - -/* fix misaligned right buttons on box headers */ -.ui.attached.header > .ui.right { - position: absolute; - right: 0.78571429rem; - top: 0; - bottom: 0; - display: flex; - align-items: center; - gap: 0.25em; -} - -/* the default ".ui.attached.header > .ui.right" is only able to contain "tiny" buttons, other buttons are too large */ -.ui.attached.header > .ui.right .ui.tiny.button { - padding: 6px 10px; - font-weight: var(--font-weight-normal); -} - -/* if a .top.attached.header is followed by a .segment, add some margin */ -.ui.segments + .ui.top.attached.header, -.ui.attached.segment + .ui.top.attached.header { - margin-top: 1rem; -} - .rss-icon { display: inline-flex; color: var(--color-text-light-1); @@ -1833,35 +1384,15 @@ table th[data-sortt-desc] .svg { vertical-align: -0.15em; } -/* for the jquery.minicolors plugin */ -.minicolors-panel { - background: var(--color-secondary-dark-1) !important; -} - -/* https://github.com/go-gitea/gitea/pull/11486 */ -.ui.sub.header { - text-transform: none; -} - .ui.tabular.menu { border-color: var(--color-secondary); } -.ui.tabular.menu .item { - padding: 11px 12px; - color: var(--color-text-light-2); -} - -.ui.tabular.menu .item:hover { - color: var(--color-text); -} - .ui.tabular.menu .active.item, .ui.tabular.menu .active.item:hover { background: var(--color-body); border-color: var(--color-secondary); color: var(--color-text); - margin-top: 1px; /* offset fomantic's margin-bottom: -1px */ } .ui.segment .ui.tabular.menu .active.item, @@ -1873,31 +1404,34 @@ table th[data-sortt-desc] .svg { border-color: var(--color-secondary); } +.ui.tabular.menu .item, .ui.secondary.pointing.menu .item { + padding: 11.55px 12px !important; /* match .dashboard-navbar in height */ color: var(--color-text-light-2); } +.ui.tabular.menu .item:hover, +.ui.secondary.pointing.menu a.item:hover { + color: var(--color-text); +} + .ui.secondary.pointing.menu .active.item, .ui.secondary.pointing.menu .active.item:hover, -.ui.secondary.pointing.menu .dropdown.item:hover, -.ui.secondary.pointing.menu a.item:hover { +.ui.secondary.pointing.menu .dropdown.item:hover { color: var(--color-text-dark); } -.ui.header { - color: var(--color-text); +.ui.tabular.menu .active.item, +.ui.secondary.pointing.menu .active.item, +.resize-for-semibold::before { + font-weight: var(--font-weight-semibold); } -.ui.header .ui.label { - margin-left: 0.25rem; -} - -.ui.header > .ui.label.compact { - margin-top: inherit; -} - -.ui.header .sub.header { - color: var(--color-text-light-1); +.resize-for-semibold::before { + content: attr(data-text); + visibility: hidden; + display: block; + height: 0; } .flash-error details code, @@ -1992,8 +1526,6 @@ table th[data-sortt-desc] .svg { .btn, .ui.ui.button, .ui.ui.dropdown, -.ui.ui.label, -.flex-items-inline > .item, .flex-text-inline { display: inline-flex; align-items: center; @@ -2009,24 +1541,21 @@ table th[data-sortt-desc] .svg { vertical-align: middle; } -.ui.ui.circular.label { - justify-content: center; -} - .ui.ui.labeled.button { gap: 0; align-items: stretch; } -.ui.ui.icon.input .icon { - display: flex; - align-items: center; - justify-content: center; -} - .flex-items-block > .item, .flex-text-block { display: flex; align-items: center; gap: .25rem; } + +/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content */ +.ui.dropdown .menu.flex-items-menu > .item { + display: flex !important; + align-items: center; + gap: .5rem; +} diff --git a/web_src/css/chroma/base.css b/web_src/css/chroma/base.css index 26d128775f..bce13332f8 100644 --- a/web_src/css/chroma/base.css +++ b/web_src/css/chroma/base.css @@ -1,7 +1,3 @@ -.chroma { - background-color: var(--color-code-bg); -} - /* LineTableTD */ .chroma .lntd { vertical-align: top; diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css index 51ddd45e31..d61e0c1cf2 100644 --- a/web_src/css/dashboard.css +++ b/web_src/css/dashboard.css @@ -28,23 +28,6 @@ width: 75%; } -.dashboard.feeds .filter.menu .item .floating.label, -.dashboard.issues .filter.menu .item .floating.label { - top: 7px; - left: 90%; - width: 15%; -} - -@media (max-width: 767.98px) { - .dashboard.feeds .filter.menu .item .floating.label, - .dashboard.issues .filter.menu .item .floating.label { - top: 10px; - left: auto; - width: auto; - right: 13px; - } -} - /* Sort */ .dashboard.feeds .filter.menu .jump.item, .dashboard.issues .filter.menu .jump.item { @@ -77,15 +60,14 @@ margin: 0 1px; /* Accommodate for Semantic's 1px hacks on .attached elements */ } -.dashboard .dashboard-navbar { - padding-left: 0.5rem; - padding-right: 0.5rem; +.dashboard .secondary-nav { + padding: 1px 12px; /* match .overflow-menu-items in height */ } -.dashboard .dashboard-navbar .org-visibility .label { +.dashboard .secondary-nav .org-visibility .label { margin-left: 5px; } -.dashboard .dashboard-navbar .ui.dropdown { +.dashboard .secondary-nav .ui.dropdown { max-width: 100%; } diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css index 12ec1866a5..8a2f4ea416 100644 --- a/web_src/css/editor/combomarkdowneditor.css +++ b/web_src/css/editor/combomarkdowneditor.css @@ -37,13 +37,12 @@ .combo-markdown-editor textarea.markdown-text-editor { display: block; width: 100%; - min-height: 200px; - max-height: calc(100vh - 200px); + max-height: calc(100vh - var(--min-height-textarea)); resize: vertical; } .combo-markdown-editor .CodeMirror-scroll { - max-height: calc(100vh - 200px); + max-height: calc(100vh - var(--min-height-textarea)); } /* use the same styles as markup/content.css */ diff --git a/web_src/css/explore.css b/web_src/css/explore.css index 08858337c0..5cdee823c0 100644 --- a/web_src/css/explore.css +++ b/web_src/css/explore.css @@ -1,10 +1,8 @@ -.explore .navbar { - margin-bottom: 15px !important; - background-color: var(--color-header-wrapper) !important; +.explore .secondary-nav { border-width: 1px !important; } -.explore .navbar .svg { +.explore .secondary-nav .svg { width: 16px; text-align: center; margin-right: 5px; diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css new file mode 100644 index 0000000000..b7436783df --- /dev/null +++ b/web_src/css/features/colorpicker.css @@ -0,0 +1,47 @@ +.js-color-picker-input { + display: flex; + position: relative; +} + +.js-color-picker-input input { + padding-top: 8px !important; + padding-bottom: 8px !important; + padding-left: 32px !important; +} + +.js-color-picker-input .preview-square { + position: absolute; + aspect-ratio: 1; + height: 16px; + left: 10px; + top: 50%; + transform: translateY(-50%); + border-radius: 2px; + background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */ + background-position: 0 0, 4px 4px; + background-size: 8px 8px; +} + +.js-color-picker-input .preview-square::after { + content: ""; + position: absolute; + width: 100%; + height: 100%; + border-radius: inherit; + background-color: currentcolor; +} + +hex-color-picker { + width: 180px; + height: 120px; +} + +hex-color-picker::part(hue-pointer), +hex-color-picker::part(saturation-pointer) { + width: 22px; + height: 22px; +} + +hex-color-picker::part(hue) { + flex-basis: 16px; +} diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css index 795e1f2d61..6a04c44e51 100644 --- a/web_src/css/features/gitgraph.css +++ b/web_src/css/features/gitgraph.css @@ -4,12 +4,6 @@ min-height: 350px; } -#git-graph-container > .ui.segment.loading { - border: 0; - z-index: 1; - min-height: 246px; -} - #git-graph-container h2 { display: flex; justify-content: space-between; diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index f85430a2a8..e23c146748 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -19,6 +19,11 @@ overflow: visible; display: flex; flex-direction: column; + cursor: default; +} + +.project-column .issue-card { + color: var(--color-text); } .project-column-header { @@ -27,25 +32,15 @@ justify-content: space-between; } -.project-column-header.dark-label { - color: var(--color-project-board-dark-label) !important; -} - -.project-column-header.dark-label .project-column-title { - color: var(--color-project-board-dark-label) !important; -} - -.project-column-header.light-label { - color: var(--color-project-board-light-label) !important; -} - -.project-column-header.light-label .project-column-title { - color: var(--color-project-board-light-label) !important; -} - .project-column-title { background: none !important; line-height: 1.25 !important; + cursor: inherit; +} + +.project-column-title, +.project-column-issue-count { + color: inherit !important; } .project-column > .cards { @@ -62,6 +57,8 @@ .project-column > .divider { margin: 5px 0; + border-color: currentcolor; + opacity: .5; } .project-column:first-child { @@ -92,6 +89,7 @@ } .card-ghost { + border-color: var(--color-secondary-dark-4) !important; border-style: dashed !important; background: none !important; } @@ -99,26 +97,3 @@ .card-ghost * { opacity: 0; } - -.color-field .minicolors.minicolors-theme-default { - display: block; -} - -.color-field .minicolors.minicolors-theme-default .minicolors-input { - height: 38px; - padding-left: 2rem; -} - -.color-field .minicolors.minicolors-theme-default .minicolors-swatch { - top: 10px; -} - -.edit-project-column-modal .color.picker.column, -.new-project-column-modal .color.picker.column { - display: flex; -} - -.edit-project-column-modal .color.picker.column .minicolors, -.new-project-column-modal .color.picker.column .minicolors { - flex: 1; -} diff --git a/web_src/css/form.css b/web_src/css/form.css index e4efa34948..a8f73b6b66 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -32,15 +32,17 @@ textarea, .ui.form input[type="text"], .ui.form input[type="time"], .ui.form input[type="url"], -.ui.selection.dropdown, -.ui.checkbox label::before, -.ui.checkbox input:checked ~ label::before, -.ui.checkbox input:not([type="radio"]):indeterminate ~ label::before { +.ui.selection.dropdown { background: var(--color-input-background); border-color: var(--color-input-border); color: var(--color-input-text); } +/* fix fomantic small dropdown having inconsistent padding with input */ +.ui.small.selection.dropdown { + padding: .67857143em 3.2em .67857143em 1em; +} + input:hover, textarea:hover, .ui.input input:hover, @@ -58,12 +60,7 @@ textarea:hover, .ui.form input[type="text"]:hover, .ui.form input[type="time"]:hover, .ui.form input[type="url"]:hover, -.ui.selection.dropdown:hover, -.ui.checkbox label:hover::before, -.ui.checkbox label:active::before, -.ui.radio.checkbox label::after, -.ui.radio.checkbox input:focus ~ label::before, -.ui.radio.checkbox input:checked ~ label::before { +.ui.selection.dropdown:hover { background: var(--color-input-background); border-color: var(--color-input-border-hover); color: var(--color-input-text); @@ -86,11 +83,7 @@ textarea:focus, .ui.form input[type="text"]:focus, .ui.form input[type="time"]:focus, .ui.form input[type="url"]:focus, -.ui.selection.dropdown:focus, -.ui.checkbox input:focus ~ label::before, -.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::before, -.ui.checkbox input:checked:focus ~ label::before, -.ui.radio.checkbox input:focus:checked ~ label::before { +.ui.selection.dropdown:focus { background: var(--color-input-background); border-color: var(--color-primary); color: var(--color-input-text); @@ -101,58 +94,21 @@ textarea:focus, .ui.form .inline.fields .field > label, .ui.form .inline.fields .field > p, .ui.form .inline.field > label, -.ui.form .inline.field > p, -.ui.checkbox label, -.ui.checkbox + label, -.ui.checkbox label:hover, -.ui.checkbox + label:hover, -.ui.checkbox input:focus ~ label, -.ui.checkbox input:active ~ label { +.ui.form .inline.field > p { color: var(--color-text); } .ui.form .required.fields:not(.grouped) > .field > label::after, .ui.form .required.fields.grouped > label::after, .ui.form .required.field > label::after, -.ui.form .required.fields:not(.grouped) > .field > .checkbox::after, -.ui.form .required.field > .checkbox::after, .ui.form label.required::after { color: var(--color-red); } -.ui.input, -.ui.checkbox input:focus ~ label::after, -.ui.checkbox input:checked ~ label::after, -.ui.checkbox label:active::after, -.ui.checkbox input:not([type="radio"]):indeterminate ~ label::after, -.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::after, -.ui.checkbox input:checked:focus ~ label::after, -.ui.disabled.checkbox label, -.ui.checkbox input[disabled] ~ label { +.ui.input { color: var(--color-input-text); } -.ui.radio.checkbox input:focus ~ label::after, -.ui.radio.checkbox input:checked ~ label::after, -.ui.radio.checkbox input:focus:checked ~ label::after { - background: var(--color-input-text); -} - -.ui.toggle.checkbox label::before { - background: var(--color-input-toggle-background); -} - -.ui.toggle.checkbox label, -.ui.toggle.checkbox input:checked ~ label, -.ui.toggle.checkbox input:focus:checked ~ label { - color: var(--color-text) !important; -} - -.ui.toggle.checkbox input:checked ~ label::before, -.ui.toggle.checkbox input:focus:checked ~ label::before { - background: var(--color-primary) !important; -} - /* match <select> padding to <input> */ .ui.form select { padding: 0.67857143em 1em; @@ -239,11 +195,8 @@ textarea:focus, } } -.user.activate form, .user.forgot.password form, .user.reset.password form, -.user.link-account form, -.user.signin form, .user.signup form { margin: auto; width: 700px !important; @@ -275,12 +228,7 @@ textarea:focus, .user.signup form .header { padding-left: 280px !important; } - .user.activate form .inline.field > label, - .user.forgot.password form .inline.field > label, - .user.reset.password form .inline.field > label, - .user.link-account form .inline.field > label, - .user.signin form .inline.field > label, - .user.signup form .inline.field > label { + .user.activate form .inline.field > label { text-align: right; width: 250px !important; word-wrap: break-word; @@ -301,21 +249,6 @@ textarea:focus, .user.signup form .optional .title { margin-left: 250px !important; } - .user.activate form .inline.field > input, - .user.forgot.password form .inline.field > input, - .user.reset.password form .inline.field > input, - .user.link-account form .inline.field > input, - .user.signin form .inline.field > input, - .user.signup form .inline.field > input, - .user.activate form .inline.field > textarea, - .user.forgot.password form .inline.field > textarea, - .user.reset.password form .inline.field > textarea, - .user.link-account form .inline.field > textarea, - .user.signin form .inline.field > textarea, - .user.signup form .inline.field > textarea, - .oauth-login-link { - width: 50%; - } } @media (max-width: 767.98px) { @@ -362,14 +295,7 @@ textarea:focus, .user.reset.password form .inline.field > label, .user.link-account form .inline.field > label, .user.signin form .inline.field > label, - .user.signup form .inline.field > label, - .user.activate form input, - .user.forgot.password form input, - .user.reset.password form input, - .user.link-account form input, - .user.signin form input, - .user.signup form input, - .oauth-login-link { + .user.signup form .inline.field > label { width: 100% !important; } } @@ -487,9 +413,9 @@ textarea:focus, .repository.new.repo form label, .repository.new.migrate form label, .repository.new.fork form label, - .repository.new.repo form input, - .repository.new.migrate form input, - .repository.new.fork form input, + .repository.new.repo form .inline.field > input, + .repository.new.migrate form .inline.field > input, + .repository.new.fork form .inline.field > input, .repository.new.fork form .field a, .repository.new.repo form .selection.dropdown, .repository.new.migrate form .selection.dropdown, diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css index da94ebb486..118c058b19 100644 --- a/web_src/css/helpers.css +++ b/web_src/css/helpers.css @@ -2,21 +2,6 @@ Gitea's tailwind-style CSS helper classes have `gt-` prefix. Gitea's private styles use `g-` prefix. */ -.gt-df { display: flex !important; } -.gt-dib { display: inline-block !important; } -.gt-ac { align-items: center !important; } -.gt-jc { justify-content: center !important; } -.gt-je { justify-content: flex-end !important; } -.gt-sb { justify-content: space-between !important; } -.gt-fc { flex-direction: column !important; } -.gt-f1 { flex: 1 !important; } -.gt-fw { flex-wrap: wrap !important; } -.gt-vm { vertical-align: middle !important; } - -.gt-mono { - font-family: var(--fonts-monospace) !important; - font-size: .95em !important; /* compensate for monospace fonts being usually slightly larger */ -} .gt-word-break { word-wrap: break-word !important; @@ -46,82 +31,6 @@ Gitea's private styles use `g-` prefix. text-overflow: ellipsis; } -.gt-max-width-12rem { max-width: 12rem !important; } -.gt-max-width-24rem { max-width: 24rem !important; } - -/* below class names match Tailwind CSS */ -.gt-break-all { word-break: break-all !important; } -.gt-content-center { align-content: center !important; } -.gt-cursor-default { cursor: default !important; } -.gt-cursor-pointer { cursor: pointer !important; } -.gt-cursor-grab { cursor: grab !important; } -.gt-invisible { visibility: hidden !important; } -.gt-items-start { align-items: flex-start !important; } -.gt-pointer-events-none { pointer-events: none !important; } -.gt-relative { position: relative !important; } -.gt-whitespace-nowrap { white-space: nowrap !important; } -.gt-whitespace-pre { white-space: pre !important; } -.gt-whitespace-pre-wrap { white-space: pre-wrap !important; } -.gt-object-contain { object-fit: contain !important; } -.gt-self-center { align-self: center !important; } -.gt-self-start { align-self: flex-start !important; } -.gt-self-end { align-self: flex-end !important; } -.gt-no-underline { text-decoration-line: none !important; } -.gt-normal-case { text-transform: none !important; } -.gt-italic { font-style: italic !important; } -.gt-overflow-x-auto { overflow-x: auto !important; } -.gt-overflow-x-scroll { overflow-x: scroll !important; } -.gt-overflow-y-hidden { overflow-y: hidden !important; } - -.gt-h-screen { height: 100vh !important; } -.gt-h-full { height: 100% !important; } -.gt-w-auto { width: auto !important; } -.gt-w-screen { width: 100vw !important; } -.gt-w-full { width: 100% !important; } - -.gt-float-left { float: left !important; } -.gt-float-right { float: right !important; } -.gt-clear-both { clear: both !important; } - -.gt-text-center { text-align: center !important; } -.gt-text-left { text-align: left !important; } -.gt-text-right { text-align: right !important; } - -.gt-font-light { font-weight: var(--font-weight-light) !important; } -.gt-font-normal { font-weight: var(--font-weight-normal) !important; } -.gt-font-medium { font-weight: var(--font-weight-medium) !important; } -.gt-font-semibold { font-weight: var(--font-weight-semibold) !important; } -.gt-font-bold { font-weight: var(--font-weight-bold) !important; } - -.gt-rounded { border-radius: var(--border-radius) !important; } -.gt-rounded-top { border-radius: var(--border-radius) var(--border-radius) 0 0 !important; } -.gt-rounded-bottom { border-radius: 0 0 var(--border-radius) var(--border-radius) !important; } -.gt-rounded-left { border-radius: var(--border-radius) 0 0 var(--border-radius) !important; } -.gt-rounded-right { border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; } - -.gt-border-secondary { border: 1px solid var(--color-secondary) !important; } -.gt-border-secondary-top { border-top: 1px solid var(--color-secondary) !important; } -.gt-border-secondary-bottom { border-bottom: 1px solid var(--color-secondary) !important; } -.gt-border-secondary-left { border-left: 1px solid var(--color-secondary) !important; } -.gt-border-secondary-right { border-right: 1px solid var(--color-secondary) !important; } - -.gt-bg-red { background: var(--color-red) !important; } -.gt-bg-orange { background: var(--color-orange) !important; } -.gt-bg-yellow { background: var(--color-yellow) !important; } -.gt-bg-olive { background: var(--color-olive) !important; } -.gt-bg-green { background: var(--color-green) !important; } -.gt-bg-teal { background: var(--color-teal) !important; } -.gt-bg-blue { background: var(--color-blue) !important; } -.gt-bg-violet { background: var(--color-violet) !important; } -.gt-bg-purple { background: var(--color-purple) !important; } -.gt-bg-pink { background: var(--color-pink) !important; } -.gt-bg-brown { background: var(--color-brown) !important; } -.gt-bg-grey { background: var(--color-grey) !important; } -.gt-bg-gold { background: var(--color-gold) !important; } -.gt-bg-transparent { background: transparent !important; } - -.gt-text-white { color: var(--color-white) !important; } - .interact-fg { color: inherit !important; } .interact-fg:hover { color: var(--color-primary) !important; } .interact-fg:active { color: var(--color-primary-active) !important; } @@ -130,152 +39,44 @@ Gitea's private styles use `g-` prefix. .interact-bg:hover { background: var(--color-hover) !important; } .interact-bg:active { background: var(--color-active) !important; } -.gt-m-0 { margin: 0 !important; } -.gt-m-1 { margin: .125rem !important; } -.gt-m-2 { margin: .25rem !important; } -.gt-m-3 { margin: .5rem !important; } -.gt-m-4 { margin: 1rem !important; } -.gt-m-5 { margin: 2rem !important; } - -.gt-ml-0 { margin-left: 0 !important; } -.gt-ml-1 { margin-left: .125rem !important; } -.gt-ml-2 { margin-left: .25rem !important; } -.gt-ml-3 { margin-left: .5rem !important; } -.gt-ml-4 { margin-left: 1rem !important; } -.gt-ml-5 { margin-left: 2rem !important; } - -.gt-mr-0 { margin-right: 0 !important; } -.gt-mr-1 { margin-right: .125rem !important; } -.gt-mr-2 { margin-right: .25rem !important; } -.gt-mr-3 { margin-right: .5rem !important; } -.gt-mr-4 { margin-right: 1rem !important; } -.gt-mr-5 { margin-right: 2rem !important; } - -.gt-mt-0 { margin-top: 0 !important; } -.gt-mt-1 { margin-top: .125rem !important; } -.gt-mt-2 { margin-top: .25rem !important; } -.gt-mt-3 { margin-top: .5rem !important; } -.gt-mt-4 { margin-top: 1rem !important; } -.gt-mt-5 { margin-top: 2rem !important; } - -.gt-mb-0 { margin-bottom: 0 !important; } -.gt-mb-1 { margin-bottom: .125rem !important; } -.gt-mb-2 { margin-bottom: .25rem !important; } -.gt-mb-3 { margin-bottom: .5rem !important; } -.gt-mb-4 { margin-bottom: 1rem !important; } -.gt-mb-5 { margin-bottom: 2rem !important; } - -.gt-mx-0 { margin-left: 0 !important; margin-right: 0 !important; } -.gt-mx-1 { margin-left: .125rem !important; margin-right: .125rem !important; } -.gt-mx-2 { margin-left: .25rem !important; margin-right: .25rem !important; } -.gt-mx-3 { margin-left: .5rem !important; margin-right: .5rem !important; } -.gt-mx-4 { margin-left: 1rem !important; margin-right: 1rem !important; } -.gt-mx-5 { margin-left: 2rem !important; margin-right: 2rem !important; } - -.gt-my-0 { margin-top: 0 !important; margin-bottom: 0 !important; } -.gt-my-1 { margin-top: .125rem !important; margin-bottom: .125rem !important; } -.gt-my-2 { margin-top: .25rem !important; margin-bottom: .25rem !important; } -.gt-my-3 { margin-top: .5rem !important; margin-bottom: .5rem !important; } -.gt-my-4 { margin-top: 1rem !important; margin-bottom: 1rem !important; } -.gt-my-5 { margin-top: 2rem !important; margin-bottom: 2rem !important; } - -.gt-m-auto { margin: auto !important; } -.gt-mx-auto { margin-left: auto !important; margin-right: auto !important; } -.gt-my-auto { margin-top: auto !important; margin-bottom: auto !important; } -.gt-mt-auto { margin-top: auto !important; } -.gt-mr-auto { margin-right: auto !important; } -.gt-mb-auto { margin-bottom: auto !important; } -.gt-ml-auto { margin-left: auto !important; } - -.gt-p-0 { padding: 0 !important; } -.gt-p-1 { padding: .125rem !important; } -.gt-p-2 { padding: .25rem !important; } -.gt-p-3 { padding: .5rem !important; } -.gt-p-4 { padding: 1rem !important; } -.gt-p-5 { padding: 2rem !important; } - -.gt-pl-0 { padding-left: 0 !important; } -.gt-pl-1 { padding-left: .125rem !important; } -.gt-pl-2 { padding-left: .25rem !important; } -.gt-pl-3 { padding-left: .5rem !important; } -.gt-pl-4 { padding-left: 1rem !important; } -.gt-pl-5 { padding-left: 2rem !important; } - -.gt-pr-0 { padding-right: 0 !important; } -.gt-pr-1 { padding-right: .125rem !important; } -.gt-pr-2 { padding-right: .25rem !important; } -.gt-pr-3 { padding-right: .5rem !important; } -.gt-pr-4 { padding-right: 1rem !important; } -.gt-pr-5 { padding-right: 2rem !important; } - -.gt-pt-0 { padding-top: 0 !important; } -.gt-pt-1 { padding-top: .125rem !important; } -.gt-pt-2 { padding-top: .25rem !important; } -.gt-pt-3 { padding-top: .5rem !important; } -.gt-pt-4 { padding-top: 1rem !important; } -.gt-pt-5 { padding-top: 2rem !important; } - -.gt-pb-0 { padding-bottom: 0 !important; } -.gt-pb-1 { padding-bottom: .125rem !important; } -.gt-pb-2 { padding-bottom: .25rem !important; } -.gt-pb-3 { padding-bottom: .5rem !important; } -.gt-pb-4 { padding-bottom: 1rem !important; } -.gt-pb-5 { padding-bottom: 2rem !important; } - -.gt-px-0 { padding-left: 0 !important; padding-right: 0 !important; } -.gt-px-1 { padding-left: .125rem !important; padding-right: .125rem !important; } -.gt-px-2 { padding-left: .25rem !important; padding-right: .25rem !important; } -.gt-px-3 { padding-left: .5rem !important; padding-right: .5rem !important; } -.gt-px-4 { padding-left: 1rem !important; padding-right: 1rem !important; } -.gt-px-5 { padding-left: 2rem !important; padding-right: 2rem !important; } - -.gt-py-0 { padding-top: 0 !important; padding-bottom: 0 !important; } -.gt-py-1 { padding-top: .125rem !important; padding-bottom: .125rem !important; } -.gt-py-2 { padding-top: .25rem !important; padding-bottom: .25rem !important; } -.gt-py-3 { padding-top: .5rem !important; padding-bottom: .5rem !important; } -.gt-py-4 { padding-top: 1rem !important; padding-bottom: 1rem !important; } -.gt-py-5 { padding-top: 2rem !important; padding-bottom: 2rem !important; } - -.gt-gap-0 { gap: 0 !important; } -.gt-gap-1 { gap: .125rem !important; } -.gt-gap-2 { gap: .25rem !important; } -.gt-gap-3 { gap: .5rem !important; } -.gt-gap-4 { gap: 1rem !important; } -.gt-gap-5 { gap: 2rem !important; } - -.gt-gap-x-0 { column-gap: 0 !important; } -.gt-gap-x-1 { column-gap: .125rem !important; } -.gt-gap-x-2 { column-gap: .25rem !important; } -.gt-gap-x-3 { column-gap: .5rem !important; } -.gt-gap-x-4 { column-gap: 1rem !important; } -.gt-gap-x-5 { column-gap: 2rem !important; } - -.gt-gap-y-0 { row-gap: 0 !important; } -.gt-gap-y-1 { row-gap: .125rem !important; } -.gt-gap-y-2 { row-gap: .25rem !important; } -.gt-gap-y-3 { row-gap: .5rem !important; } -.gt-gap-y-4 { row-gap: 1rem !important; } -.gt-gap-y-5 { row-gap: 2rem !important; } - -.gt-shrink-0 { flex-shrink: 0 !important; } - -.gt-font-12 { font-size: 12px !important } -.gt-font-13 { font-size: 13px !important } -.gt-font-14 { font-size: 14px !important } -.gt-font-15 { font-size: 15px !important } -.gt-font-16 { font-size: 16px !important } -.gt-font-17 { font-size: 17px !important } -.gt-font-18 { font-size: 18px !important } - /* -gt-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element. +tw-hidden must win all other "display: xxx !important" classes to get the chance to "hide" an element. do not use: * "[hidden]" attribute: it's too weak, can not be applied to an element with "display: flex" * ".hidden" class: it has been polluted by Fomantic UI in many cases * inline style="display: none": it's difficult to tweak * jQuery's show/hide/toggle: it can not show/hide elements with "display: xxx !important" only use: -* this ".gt-hidden" class +* this ".tw-hidden" class * showElem/hideElem/toggleElem functions in "utils/dom.js" */ -.gt-hidden.gt-hidden { display: none !important; } +.tw-hidden.tw-hidden { display: none !important; } + +@media (max-width: 767.98px) { + /* double selector so it wins over .tw-flex (old .gt-df) etc */ + .not-mobile.not-mobile { + display: none !important; + } +} +@media (min-width: 767.98px) { + .only-mobile.only-mobile { + display: none !important; + } +} + +.tab-size-1 { tab-size: 1 !important; } +.tab-size-2 { tab-size: 2 !important; } +.tab-size-3 { tab-size: 3 !important; } +.tab-size-4 { tab-size: 4 !important; } +.tab-size-5 { tab-size: 5 !important; } +.tab-size-6 { tab-size: 6 !important; } +.tab-size-7 { tab-size: 7 !important; } +.tab-size-8 { tab-size: 8 !important; } +.tab-size-9 { tab-size: 9 !important; } +.tab-size-10 { tab-size: 10 !important; } +.tab-size-11 { tab-size: 11 !important; } +.tab-size-12 { tab-size: 12 !important; } +.tab-size-13 { tab-size: 13 !important; } +.tab-size-14 { tab-size: 14 !important; } +.tab-size-15 { tab-size: 15 !important; } +.tab-size-16 { tab-size: 16 !important; } diff --git a/web_src/css/index.css b/web_src/css/index.css index f893531b78..ad59f32636 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -1,15 +1,28 @@ @import "./modules/normalize.css"; @import "./modules/animations.css"; + +/* fomantic replacements */ @import "./modules/button.css"; +@import "./modules/container.css"; +@import "./modules/divider.css"; +@import "./modules/header.css"; +@import "./modules/input.css"; +@import "./modules/label.css"; +@import "./modules/list.css"; +@import "./modules/segment.css"; +@import "./modules/grid.css"; +@import "./modules/message.css"; +@import "./modules/table.css"; +@import "./modules/card.css"; +@import "./modules/checkbox.css"; +@import "./modules/modal.css"; + @import "./modules/select.css"; @import "./modules/tippy.css"; -@import "./modules/modal.css"; @import "./modules/breadcrumb.css"; -@import "./modules/card.css"; @import "./modules/comment.css"; @import "./modules/navbar.css"; @import "./modules/toast.css"; -@import "./modules/divider.css"; @import "./modules/svg.css"; @import "./modules/flexcontainer.css"; @@ -29,6 +42,7 @@ @import "./markup/content.css"; @import "./markup/codecopy.css"; +@import "./markup/codepreview.css"; @import "./markup/asciicast.css"; @import "./chroma/base.css"; @@ -59,4 +73,6 @@ @import "./explore.css"; @import "./review.css"; @import "./actions.css"; + +@tailwind utilities; @import "./helpers.css"; diff --git a/web_src/css/markup/codepreview.css b/web_src/css/markup/codepreview.css new file mode 100644 index 0000000000..c9d19f5cc8 --- /dev/null +++ b/web_src/css/markup/codepreview.css @@ -0,0 +1,36 @@ +.markup .code-preview-container { + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + margin: 0.25em 0; +} + +.markup .code-preview-container .code-preview-header { + border-bottom: 1px solid var(--color-secondary); + padding: 0.5em; + font-size: 12px; +} + +.markup .code-preview-container table { + width: 100%; + max-height: 240px; /* 12 lines at 20px per line */ + overflow-y: auto; + margin: 0; /* override ".markup table {margin}" */ +} + +/* workaround to hide empty p before container - more details are in "html_codepreview.go" */ +.markup p:empty:has(+ .code-preview-container) { + display: none; +} + +/* override the polluted styles from the content.css: ".markup table ..." */ +.markup .code-preview-container table tr { + border: 0 !important; +} +.markup .code-preview-container table th, +.markup .code-preview-container table td { + border: 0 !important; + padding: 0 0 0 5px !important; +} +.markup .code-preview-container table tr:nth-child(2n) { + background: none !important; +} diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index caefa1605c..6ba4e40072 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -270,7 +270,7 @@ margin-left: 0; padding: 0 15px; color: var(--color-text-light-2); - border-left: 4px solid var(--color-secondary); + border-left: 0.25em solid var(--color-secondary); } .markup blockquote > :first-child { @@ -382,7 +382,7 @@ text-align: center; } -.markup span.align-center span img +.markup span.align-center span img, .markup span.align-center span video { margin: 0 auto; text-align: center; @@ -438,7 +438,7 @@ margin: 0; font-size: 85%; white-space: break-spaces; - background-color: var(--color-markup-code-block); + background-color: var(--color-markup-code-inline); border-radius: var(--border-radius); } @@ -508,7 +508,7 @@ line-height: 10px; color: var(--color-text-light); vertical-align: middle; - background-color: var(--color-markup-code-block); + background-color: var(--color-markup-code-inline); border: 1px solid var(--color-secondary); border-radius: var(--border-radius); box-shadow: inset 0 -1px 0 var(--color-secondary); diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index 87eb6a75cf..361618c449 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -6,13 +6,17 @@ .is-loading { pointer-events: none !important; position: relative !important; - overflow: hidden !important; } .is-loading > * { opacity: 0.3; } +.btn.is-loading > *, +.button.is-loading > * { + opacity: 0; +} + .is-loading::after { content: ""; position: absolute; @@ -20,6 +24,7 @@ left: 50%; top: 50%; height: min(4em, 66.6%); + width: fit-content; /* compat: safari - https://bugs.webkit.org/show_bug.cgi?id=267625 */ aspect-ratio: 1; transform: translate(-50%, -50%); animation: isloadingspin 1000ms infinite linear; @@ -29,10 +34,14 @@ border-radius: var(--border-radius-circle); } -.is-loading.small-loading-icon::after { +.is-loading.loading-icon-2px::after { border-width: 2px; } +.is-loading.loading-icon-3px::after { + border-width: 3px; +} + /* for single form button, the loading state should be on the button, but not go semi-transparent, just replace the text on the button with the loader. */ form.single-button-form.is-loading > * { opacity: 1; @@ -57,7 +66,7 @@ form.single-button-form.is-loading .button { background: transparent; } -/* TODO: not needed, use "is-loading small-loading-icon" instead */ +/* TODO: not needed, use "is-loading loading-icon-2px" instead */ code.language-math.is-loading::after { padding: 0; border-width: 2px; diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css index 36cb499aeb..faeed8c9a1 100644 --- a/web_src/css/modules/button.css +++ b/web_src/css/modules/button.css @@ -61,11 +61,22 @@ It needs some tricks to tweak the left/right borders with active state */ border-right: none; } -.ui.buttons .button:first-child { +.ui.buttons .button:hover { + border-color: var(--color-secondary-dark-2); +} + +.ui.buttons .button:hover + .button { + border-left: 1px solid var(--color-secondary-dark-2); +} + +/* TODO: these "tw-hidden" selectors are only used by "blame.tmpl" buttons: Raw/Normal View/History/Unescape, need to be refactored to a clear solution later */ +.ui.buttons .button:first-child, +.ui.buttons .button.tw-hidden:first-child + .button { border-left: 1px solid var(--color-light-border); } -.ui.buttons .button:last-child { +.ui.buttons .button:last-child, +.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) { border-right: 1px solid var(--color-light-border); } @@ -85,6 +96,13 @@ It needs some tricks to tweak the left/right borders with active state */ box-shadow: none; } +/* apply the vertical padding of .compact to non-compact buttons when they contain a svg as they + would otherwise appear too large. Seen on "RSS Feed" button on repo releases tab. */ +.ui.small.button:not(.compact):has(.svg) { + padding-top: 0.58928571em; + padding-bottom: 0.58928571em; +} + .ui.labeled.button.disabled > .button, .ui.basic.buttons .button, .ui.basic.button, @@ -98,6 +116,7 @@ It needs some tricks to tweak the left/right borders with active state */ .ui.basic.button:hover { color: var(--color-text); background: var(--color-hover); + border-color: var(--color-secondary-dark-2); } .ui.basic.buttons .button:active, diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css new file mode 100644 index 0000000000..d3e45714a4 --- /dev/null +++ b/web_src/css/modules/checkbox.css @@ -0,0 +1,120 @@ +/* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +input[type="checkbox"], +input[type="radio"] { + width: var(--checkbox-size); + height: var(--checkbox-size); +} + +.ui.checkbox { + position: relative; + display: inline-block; + vertical-align: baseline; + min-height: var(--checkbox-size); + line-height: var(--checkbox-size); + min-width: var(--checkbox-size); + padding: 1px; +} + +.ui.checkbox input[type="checkbox"], +.ui.checkbox input[type="radio"] { + position: absolute; + top: 1px; + left: 0; + width: var(--checkbox-size); + height: var(--checkbox-size); +} + +.ui.checkbox input[type="checkbox"]:enabled, +.ui.checkbox input[type="radio"]:enabled, +.ui.checkbox label:enabled { + cursor: pointer; +} + +.ui.checkbox label { + cursor: auto; + position: relative; + display: block; + user-select: none; +} + +.ui.checkbox label, +.ui.radio.checkbox label { + margin-left: 1.85714em; +} + +.ui.checkbox + label { + vertical-align: middle; +} + +.ui.disabled.checkbox label, +.ui.checkbox input[disabled] ~ label { + cursor: default !important; + opacity: 0.5; + pointer-events: none; +} + +.ui.radio.checkbox { + min-height: var(--checkbox-size); +} + +/* "switch" styled checkbox */ + +.ui.toggle.checkbox { + min-height: 1.5rem; +} +.ui.toggle.checkbox input { + width: 3.5rem; + height: 1.5rem; + opacity: 0; + z-index: 3; +} +.ui.toggle.checkbox label { + min-height: 1.5rem; + padding-left: 4.5rem; + padding-top: 0.15em; +} +.ui.toggle.checkbox label::before { + display: block; + position: absolute; + content: ""; + z-index: 1; + top: 0; + width: 3.5rem; + height: 1.5rem; + border-radius: 500rem; + left: 0; +} +.ui.toggle.checkbox label::after { + background: var(--color-white); + position: absolute; + content: ""; + opacity: 1; + z-index: 2; + width: 1.5rem; + height: 1.5rem; + top: 0; + left: 0; + border-radius: 500rem; + transition: background 0.3s ease, left 0.3s ease; +} +.ui.toggle.checkbox input ~ label::after { + left: -0.05rem; +} +.ui.toggle.checkbox input:checked ~ label::after { + left: 2.15rem; +} +.ui.toggle.checkbox input:focus ~ label::before, +.ui.toggle.checkbox label::before { + background: var(--color-input-toggle-background); +} +.ui.toggle.checkbox label, +.ui.toggle.checkbox input:checked ~ label, +.ui.toggle.checkbox input:focus:checked ~ label { + color: var(--color-text) !important; +} +.ui.toggle.checkbox input:checked ~ label::before, +.ui.toggle.checkbox input:focus:checked ~ label::before { + background: var(--color-primary) !important; +} diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css new file mode 100644 index 0000000000..f394d6c06d --- /dev/null +++ b/web_src/css/modules/container.css @@ -0,0 +1,59 @@ +/* based on Fomantic UI container module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.container { + display: block; + max-width: 100%; +} + +@media (max-width: 767.98px) { + .ui.ui.ui.container:not(.fluid) { + width: auto; + margin-left: 1em; + margin-right: 1em; + } +} + +@media (min-width: 768px) and (max-width: 991.98px) { + .ui.ui.ui.container:not(.fluid) { + width: 723px; + margin-left: auto; + margin-right: auto; + } +} + +@media (min-width: 992px) and (max-width: 1199.98px) { + .ui.ui.ui.container:not(.fluid) { + width: 933px; + margin-left: auto; + margin-right: auto; + } +} + +@media (min-width: 1200px) { + .ui.ui.ui.container:not(.fluid) { + width: 1127px; + margin-left: auto; + margin-right: auto; + } +} + +.ui.fluid.container { + width: 100%; +} + +.ui[class*="center aligned"].container { + text-align: center; +} + +/* overwrite width of containers inside the main page content div (div with class "page-content") */ +.page-content .ui.ui.ui.container:not(.fluid) { + width: 1280px; + max-width: calc(100% - calc(2 * var(--page-margin-x))); + margin-left: auto; + margin-right: auto; +} + +.ui.container.fluid.padded { + padding: 0 var(--page-margin-x); +} diff --git a/web_src/css/modules/flexcontainer.css b/web_src/css/modules/flexcontainer.css index 0b559f1e7d..1ca513687f 100644 --- a/web_src/css/modules/flexcontainer.css +++ b/web_src/css/modules/flexcontainer.css @@ -2,7 +2,8 @@ .flex-container { display: flex !important; - gap: 16px; + gap: var(--page-spacing); + margin-top: var(--page-spacing); } .flex-container-nav { diff --git a/web_src/css/modules/grid.css b/web_src/css/modules/grid.css new file mode 100644 index 0000000000..4aaa452372 --- /dev/null +++ b/web_src/css/modules/grid.css @@ -0,0 +1,516 @@ +/* based on Fomantic UI grid module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + padding: 0; + margin-top: -1rem; + margin-bottom: -1rem; + margin-left: -1rem; + margin-right: -1rem; +} + +.ui.relaxed.grid { + margin-left: -1.5rem; + margin-right: -1.5rem; +} +.ui[class*="very relaxed"].grid { + margin-left: -2.5rem; + margin-right: -2.5rem; +} + +.ui.grid + .grid { + margin-top: 1rem; +} + +.ui.grid > .column:not(.row), +.ui.grid > .row > .column { + position: relative; + display: inline-block; + width: 6.25%; + padding-left: 1rem; + padding-right: 1rem; + vertical-align: top; +} +.ui.grid > * { + padding-left: 1rem; + padding-right: 1rem; +} + +.ui.grid > .row { + position: relative; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: inherit; + align-items: stretch; + width: 100% !important; + padding: 0; + padding-top: 1rem; + padding-bottom: 1rem; +} + +.ui.grid > .column:not(.row) { + padding-top: 1rem; + padding-bottom: 1rem; +} +.ui.grid > .row > .column { + margin-top: 0; + margin-bottom: 0; +} + +.ui.grid > .row > img, +.ui.grid > .row > .column > img { + max-width: 100%; +} + +.ui.grid > .ui.grid:first-child { + margin-top: 0; +} +.ui.grid > .ui.grid:last-child { + margin-bottom: 0; +} + +.ui.grid .aligned.row > .column > .segment:not(.compact):not(.attached), +.ui.aligned.grid .column > .segment:not(.compact):not(.attached) { + width: 100%; +} + +.ui.grid .row + .ui.divider { + flex-grow: 1; + margin: 1rem; +} +.ui.grid .column + .ui.vertical.divider { + height: calc(50% - 1rem); +} + +.ui.grid > .row > .column:last-child > .horizontal.segment, +.ui.grid > .column:last-child > .horizontal.segment { + box-shadow: none; +} + +@media only screen and (max-width: 767.98px) { + .ui.page.grid { + width: auto; + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + } +} +@media only screen and (min-width: 768px) and (max-width: 991.98px) { + .ui.page.grid { + width: auto; + margin-left: 0; + margin-right: 0; + padding-left: 2em; + padding-right: 2em; + } +} +@media only screen and (min-width: 992px) and (max-width: 1199.98px) { + .ui.page.grid { + width: auto; + margin-left: 0; + margin-right: 0; + padding-left: 3%; + padding-right: 3%; + } +} +@media only screen and (min-width: 1200px) and (max-width: 1919.98px) { + .ui.page.grid { + width: auto; + margin-left: 0; + margin-right: 0; + padding-left: 15%; + padding-right: 15%; + } +} +@media only screen and (min-width: 1920px) { + .ui.page.grid { + width: auto; + margin-left: 0; + margin-right: 0; + padding-left: 23%; + padding-right: 23%; + } +} + +.ui.grid > .column:only-child, +.ui.grid > .row > .column:only-child { + width: 100%; +} + +.ui[class*="one column"].grid > .row > .column, +.ui[class*="one column"].grid > .column:not(.row) { + width: 100%; +} +.ui[class*="two column"].grid > .row > .column, +.ui[class*="two column"].grid > .column:not(.row) { + width: 50%; +} +.ui[class*="three column"].grid > .row > .column, +.ui[class*="three column"].grid > .column:not(.row) { + width: 33.33333333%; +} +.ui[class*="four column"].grid > .row > .column, +.ui[class*="four column"].grid > .column:not(.row) { + width: 25%; +} +.ui[class*="five column"].grid > .row > .column, +.ui[class*="five column"].grid > .column:not(.row) { + width: 20%; +} +.ui[class*="six column"].grid > .row > .column, +.ui[class*="six column"].grid > .column:not(.row) { + width: 16.66666667%; +} +.ui[class*="seven column"].grid > .row > .column, +.ui[class*="seven column"].grid > .column:not(.row) { + width: 14.28571429%; +} +.ui[class*="eight column"].grid > .row > .column, +.ui[class*="eight column"].grid > .column:not(.row) { + width: 12.5%; +} +.ui[class*="nine column"].grid > .row > .column, +.ui[class*="nine column"].grid > .column:not(.row) { + width: 11.11111111%; +} +.ui[class*="ten column"].grid > .row > .column, +.ui[class*="ten column"].grid > .column:not(.row) { + width: 10%; +} +.ui[class*="eleven column"].grid > .row > .column, +.ui[class*="eleven column"].grid > .column:not(.row) { + width: 9.09090909%; +} +.ui[class*="twelve column"].grid > .row > .column, +.ui[class*="twelve column"].grid > .column:not(.row) { + width: 8.33333333%; +} +.ui[class*="thirteen column"].grid > .row > .column, +.ui[class*="thirteen column"].grid > .column:not(.row) { + width: 7.69230769%; +} +.ui[class*="fourteen column"].grid > .row > .column, +.ui[class*="fourteen column"].grid > .column:not(.row) { + width: 7.14285714%; +} +.ui[class*="fifteen column"].grid > .row > .column, +.ui[class*="fifteen column"].grid > .column:not(.row) { + width: 6.66666667%; +} +.ui[class*="sixteen column"].grid > .row > .column, +.ui[class*="sixteen column"].grid > .column:not(.row) { + width: 6.25%; +} + +.ui.grid > [class*="one column"].row > .column { + width: 100% !important; +} +.ui.grid > [class*="two column"].row > .column { + width: 50% !important; +} +.ui.grid > [class*="three column"].row > .column { + width: 33.33333333% !important; +} +.ui.grid > [class*="four column"].row > .column { + width: 25% !important; +} +.ui.grid > [class*="five column"].row > .column { + width: 20% !important; +} +.ui.grid > [class*="six column"].row > .column { + width: 16.66666667% !important; +} +.ui.grid > [class*="seven column"].row > .column { + width: 14.28571429% !important; +} +.ui.grid > [class*="eight column"].row > .column { + width: 12.5% !important; +} +.ui.grid > [class*="nine column"].row > .column { + width: 11.11111111% !important; +} +.ui.grid > [class*="ten column"].row > .column { + width: 10% !important; +} +.ui.grid > [class*="eleven column"].row > .column { + width: 9.09090909% !important; +} +.ui.grid > [class*="twelve column"].row > .column { + width: 8.33333333% !important; +} +.ui.grid > [class*="thirteen column"].row > .column { + width: 7.69230769% !important; +} +.ui.grid > [class*="fourteen column"].row > .column { + width: 7.14285714% !important; +} +.ui.grid > [class*="fifteen column"].row > .column { + width: 6.66666667% !important; +} +.ui.grid > [class*="sixteen column"].row > .column { + width: 6.25% !important; +} + +.ui.grid > .row > [class*="one wide"].column, +.ui.grid > .column.row > [class*="one wide"].column, +.ui.grid > [class*="one wide"].column, +.ui.column.grid > [class*="one wide"].column { + width: 6.25% !important; +} +.ui.grid > .row > [class*="two wide"].column, +.ui.grid > .column.row > [class*="two wide"].column, +.ui.grid > [class*="two wide"].column, +.ui.column.grid > [class*="two wide"].column { + width: 12.5% !important; +} +.ui.grid > .row > [class*="three wide"].column, +.ui.grid > .column.row > [class*="three wide"].column, +.ui.grid > [class*="three wide"].column, +.ui.column.grid > [class*="three wide"].column { + width: 18.75% !important; +} +.ui.grid > .row > [class*="four wide"].column, +.ui.grid > .column.row > [class*="four wide"].column, +.ui.grid > [class*="four wide"].column, +.ui.column.grid > [class*="four wide"].column { + width: 25% !important; +} +.ui.grid > .row > [class*="five wide"].column, +.ui.grid > .column.row > [class*="five wide"].column, +.ui.grid > [class*="five wide"].column, +.ui.column.grid > [class*="five wide"].column { + width: 31.25% !important; +} +.ui.grid > .row > [class*="six wide"].column, +.ui.grid > .column.row > [class*="six wide"].column, +.ui.grid > [class*="six wide"].column, +.ui.column.grid > [class*="six wide"].column { + width: 37.5% !important; +} +.ui.grid > .row > [class*="seven wide"].column, +.ui.grid > .column.row > [class*="seven wide"].column, +.ui.grid > [class*="seven wide"].column, +.ui.column.grid > [class*="seven wide"].column { + width: 43.75% !important; +} +.ui.grid > .row > [class*="eight wide"].column, +.ui.grid > .column.row > [class*="eight wide"].column, +.ui.grid > [class*="eight wide"].column, +.ui.column.grid > [class*="eight wide"].column { + width: 50% !important; +} +.ui.grid > .row > [class*="nine wide"].column, +.ui.grid > .column.row > [class*="nine wide"].column, +.ui.grid > [class*="nine wide"].column, +.ui.column.grid > [class*="nine wide"].column { + width: 56.25% !important; +} +.ui.grid > .row > [class*="ten wide"].column, +.ui.grid > .column.row > [class*="ten wide"].column, +.ui.grid > [class*="ten wide"].column, +.ui.column.grid > [class*="ten wide"].column { + width: 62.5% !important; +} +.ui.grid > .row > [class*="eleven wide"].column, +.ui.grid > .column.row > [class*="eleven wide"].column, +.ui.grid > [class*="eleven wide"].column, +.ui.column.grid > [class*="eleven wide"].column { + width: 68.75% !important; +} +.ui.grid > .row > [class*="twelve wide"].column, +.ui.grid > .column.row > [class*="twelve wide"].column, +.ui.grid > [class*="twelve wide"].column, +.ui.column.grid > [class*="twelve wide"].column { + width: 75% !important; +} +.ui.grid > .row > [class*="thirteen wide"].column, +.ui.grid > .column.row > [class*="thirteen wide"].column, +.ui.grid > [class*="thirteen wide"].column, +.ui.column.grid > [class*="thirteen wide"].column { + width: 81.25% !important; +} +.ui.grid > .row > [class*="fourteen wide"].column, +.ui.grid > .column.row > [class*="fourteen wide"].column, +.ui.grid > [class*="fourteen wide"].column, +.ui.column.grid > [class*="fourteen wide"].column { + width: 87.5% !important; +} +.ui.grid > .row > [class*="fifteen wide"].column, +.ui.grid > .column.row > [class*="fifteen wide"].column, +.ui.grid > [class*="fifteen wide"].column, +.ui.column.grid > [class*="fifteen wide"].column { + width: 93.75% !important; +} +.ui.grid > .row > [class*="sixteen wide"].column, +.ui.grid > .column.row > [class*="sixteen wide"].column, +.ui.grid > [class*="sixteen wide"].column, +.ui.column.grid > [class*="sixteen wide"].column { + width: 100% !important; +} + +.ui.centered.grid, +.ui.centered.grid > .row, +.ui.grid > .centered.row { + text-align: center; + justify-content: center; +} +.ui.centered.grid > .column:not(.aligned):not(.justified):not(.row), +.ui.centered.grid > .row > .column:not(.aligned):not(.justified), +.ui.grid .centered.row > .column:not(.aligned):not(.justified) { + text-align: left; +} +.ui.grid > .centered.column, +.ui.grid > .row > .centered.column { + display: block; + margin-left: auto; + margin-right: auto; +} + +.ui.relaxed.grid > .column:not(.row), +.ui.relaxed.grid > .row > .column, +.ui.grid > .relaxed.row > .column { + padding-left: 1.5rem; + padding-right: 1.5rem; +} +.ui[class*="very relaxed"].grid > .column:not(.row), +.ui[class*="very relaxed"].grid > .row > .column, +.ui.grid > [class*="very relaxed"].row > .column { + padding-left: 2.5rem; + padding-right: 2.5rem; +} + +.ui.relaxed.grid .row + .ui.divider, +.ui.grid .relaxed.row + .ui.divider { + margin-left: 1.5rem; + margin-right: 1.5rem; +} +.ui[class*="very relaxed"].grid .row + .ui.divider, +.ui.grid [class*="very relaxed"].row + .ui.divider { + margin-left: 2.5rem; + margin-right: 2.5rem; +} + +.ui[class*="middle aligned"].grid > .column:not(.row), +.ui[class*="middle aligned"].grid > .row > .column, +.ui.grid > [class*="middle aligned"].row > .column, +.ui.grid > [class*="middle aligned"].column:not(.row), +.ui.grid > .row > [class*="middle aligned"].column { + flex-direction: column; + vertical-align: middle; + align-self: center !important; +} + +.ui[class*="left aligned"].grid > .column, +.ui[class*="left aligned"].grid > .row > .column, +.ui.grid > [class*="left aligned"].row > .column, +.ui.grid > [class*="left aligned"].column.column, +.ui.grid > .row > [class*="left aligned"].column.column { + text-align: left; + align-self: inherit; +} + +.ui[class*="center aligned"].grid > .column, +.ui[class*="center aligned"].grid > .row > .column, +.ui.grid > [class*="center aligned"].row > .column, +.ui.grid > [class*="center aligned"].column.column, +.ui.grid > .row > [class*="center aligned"].column.column { + text-align: center; + align-self: inherit; +} +.ui[class*="center aligned"].grid { + justify-content: center; +} + +.ui[class*="right aligned"].grid > .column, +.ui[class*="right aligned"].grid > .row > .column, +.ui.grid > [class*="right aligned"].row > .column, +.ui.grid > [class*="right aligned"].column.column, +.ui.grid > .row > [class*="right aligned"].column.column { + text-align: right; + align-self: inherit; +} + +.ui[class*="equal width"].grid > .column:not(.row), +.ui[class*="equal width"].grid > .row > .column, +.ui.grid > [class*="equal width"].row > .column { + display: inline-block; + flex-grow: 1; +} +.ui[class*="equal width"].grid > .wide.column, +.ui[class*="equal width"].grid > .row > .wide.column, +.ui.grid > [class*="equal width"].row > .wide.column { + flex-grow: 0; +} + +@media only screen and (max-width: 767.98px) { + .ui[class*="mobile reversed"].grid, + .ui[class*="mobile reversed"].grid > .row, + .ui.grid > [class*="mobile reversed"].row { + flex-direction: row-reverse; + } + .ui.stackable[class*="mobile reversed"] { + flex-direction: column-reverse; + } +} + +@media only screen and (max-width: 767.98px) { + .ui.stackable.grid { + width: auto; + margin-left: 0 !important; + margin-right: 0 !important; + } + .ui.stackable.grid > .row > .wide.column, + .ui.stackable.grid > .wide.column, + .ui.stackable.grid > .column.grid > .column, + .ui.stackable.grid > .column.row > .column, + .ui.stackable.grid > .row > .column, + .ui.stackable.grid > .column:not(.row), + .ui.grid > .stackable.stackable.stackable.row > .column { + width: 100% !important; + margin: 0 !important; + box-shadow: none !important; + padding: 1rem; + } + .ui.stackable.grid:not(.vertically) > .row { + margin: 0; + padding: 0; + } + + .ui.container > .ui.stackable.grid > .column, + .ui.container > .ui.stackable.grid > .row > .column { + padding-left: 0 !important; + padding-right: 0 !important; + } + + .ui.grid .ui.stackable.grid, + .ui.segment:not(.vertical) .ui.stackable.page.grid { + margin-left: -1rem !important; + margin-right: -1rem !important; + } +} + +.ui.ui.ui.compact.grid > .column:not(.row), +.ui.ui.ui.compact.grid > .row > .column { + padding-left: 0.5rem; + padding-right: 0.5rem; +} +.ui.ui.ui.compact.grid > * { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.ui.ui.ui.compact.grid > .row { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.ui.ui.ui.compact.grid > .column:not(.row) { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css new file mode 100644 index 0000000000..05381e1185 --- /dev/null +++ b/web_src/css/modules/header.css @@ -0,0 +1,177 @@ +/* based on Fomantic UI header module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.header { + color: var(--color-text); + border: none; + margin: calc(2rem - 0.1428571428571429em) 0 1rem; + padding: 0; + font-family: var(--fonts-regular); + font-weight: var(--font-weight-medium); + line-height: 1.28571429; + text-transform: none; +} + +.ui.header:first-child { + margin-top: -0.14285714em; +} + +.ui.header:last-child { + margin-bottom: 0; +} + +.ui.header .ui.label { + margin-left: 0.25rem; + vertical-align: middle; +} + +.ui.header > .ui.label.compact { + margin-top: inherit; +} + +.ui.header .sub.header { + display: block; + font-weight: var(--font-weight-normal); + padding: 0; + margin: 0; + font-size: 1rem; + line-height: 1.2; + color: var(--color-text-light-1); +} + +.ui.header > i.icon { + display: table-cell; + opacity: 1; + font-size: 1.5em; + padding-top: 0; + vertical-align: middle; +} + +.ui.header > i.icon:only-child { + display: inline-block; + padding: 0; + margin-right: 0.75rem; +} + +.ui.header + p { + margin-top: 0; +} + +h2.ui.header { + font-size: 1.71428571rem; +} +h2.ui.header .sub.header { + font-size: 1.14285714rem; +} + +h4.ui.header { + font-size: 1.07142857rem; +} +h4.ui.header .sub.header { + font-size: 1rem; +} + +.ui.sub.header { + padding: 0; + margin-bottom: 0.14285714rem; + font-weight: var(--font-weight-normal); + font-size: 0.85714286em; +} + +.ui.icon.header svg { + width: 3em; + height: 3em; + float: none; + display: block; + line-height: var(--line-height-default); + padding: 0; + margin: 0 auto 0.5rem; + opacity: 1; +} + +.ui.header:not(h1,h2,h3,h4,h5,h6) { + font-size: 1.28571429em; +} + +.ui.attached.header { + position: relative; + background: var(--color-box-header); + padding: 0.78571429rem 1rem; + margin: 0 -1px; + border-radius: 0; + border: 1px solid var(--color-secondary); +} + +.ui.attached:not(.top).header { + border-top: none; +} + +.ui.top.attached.header { + border-radius: 0.28571429rem 0.28571429rem 0 0; +} + +.ui.bottom.attached.header { + border-radius: 0 0 0.28571429rem 0.28571429rem; +} + +.ui.attached.header:not(h1,h2,h3,h4,h5,h6) { + font-size: 1em; +} + +/* fix misaligned right buttons on box headers */ +.ui.attached.header > .ui.right { + position: absolute; + right: 0.78571429rem; + top: 0; + bottom: 0; + display: flex; + align-items: center; + gap: 0.25em; +} + +/* the default ".ui.attached.header > .ui.right" is only able to contain "tiny" buttons, other buttons are too large */ +.ui.attached.header > .ui.right .ui.tiny.button { + padding: 6px 10px; + font-weight: var(--font-weight-normal); +} + +/* open dropdown menus to the left in right-attached headers */ +.ui.attached.header > .ui.right .ui.dropdown .menu { + right: 0; + left: auto; +} + +/* if a .top.attached.header is followed by a .segment, add some margin */ +.ui.segments + .ui.top.attached.header, +.ui.attached.segment + .ui.top.attached.header { + margin-top: 1rem; +} + +.ui.dividing.header { + border-bottom-color: var(--color-secondary); +} + +.ui.dividing.header .sub.header { + padding-bottom: 0.21428571rem; +} + +.ui.dividing.header i.icon { + margin-bottom: 0; +} + +.ui.error.header { + background: var(--color-error-bg) !important; + color: var(--color-error-text) !important; + border-color: var(--color-error-border) !important; +} + +.ui.warning.header { + background: var(--color-warning-bg) !important; + color: var(--color-warning-text) !important; + border-color: var(--color-warning-border) !important; +} + +.attention-header { + padding: 0.5em 0.75em !important; + color: var(--color-text) !important; +} diff --git a/web_src/css/modules/input.css b/web_src/css/modules/input.css new file mode 100644 index 0000000000..18b785ac82 --- /dev/null +++ b/web_src/css/modules/input.css @@ -0,0 +1,197 @@ +/* based on Fomantic UI input module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.input { + position: relative; + font-weight: var(--font-weight-normal); + display: inline-flex; + color: var(--color-input-text); +} +.ui.input > input { + margin: 0; + max-width: 100%; + flex: 1 0 auto; + outline: none; + font-family: var(--fonts-regular); + padding: 0.67857143em 1em; + border: 1px solid var(--color-input-border); + color: var(--color-input-text); + border-radius: 0.28571429rem; + line-height: var(--line-height-default); + text-align: start; +} + +.ui.disabled.input, +.ui.input:not(.disabled) input[disabled] { + opacity: var(--opacity-disabled); +} +.ui.disabled.input > input, +.ui.input:not(.disabled) input[disabled] { + pointer-events: none; +} + +.ui.input.focus > input, +.ui.input > input:focus { + border-color: var(--color-primary); +} + +.ui.input.error > input { + background: var(--color-error-bg); + border-color: var(--color-error-border); + color: var(--color-error-text); +} + +.ui.icon.input > i.icon { + display: flex; + align-items: center; + justify-content: center; + cursor: default; + position: absolute; + text-align: center; + top: 0; + right: 0; + margin: 0; + height: 100%; + width: 2.67142857em; + opacity: 0.5; + border-radius: 0 0.28571429rem 0.28571429rem 0; + pointer-events: none; + padding: 4px; +} + +.ui.icon.input > i.icon.is-loading { + position: absolute !important; + height: 28px; + top: 4px; +} + +.ui.icon.input > i.icon.is-loading > * { + visibility: hidden; +} + +.ui.ui.ui.ui.icon.input > textarea, +.ui.ui.ui.ui.icon.input > input { + padding-right: 2.67142857em; +} +.ui.icon.input > i.link.icon { + cursor: pointer; +} +.ui.icon.input > i.circular.icon { + top: 0.35em; + right: 0.5em; +} + +.ui[class*="left icon"].input > i.icon { + right: auto; + left: 1px; + border-radius: 0.28571429rem 0 0 0.28571429rem; +} +.ui[class*="left icon"].input > i.circular.icon { + right: auto; + left: 0.5em; +} +.ui.ui.ui.ui[class*="left icon"].input > textarea, +.ui.ui.ui.ui[class*="left icon"].input > input { + padding-left: 2.67142857em; + padding-right: 1em; +} + +.ui.icon.input > textarea:focus ~ .icon, +.ui.icon.input > input:focus ~ .icon { + opacity: 1; +} + +.ui.icon.input > textarea ~ i.icon { + height: 3em; +} + +.ui.form .field.error > .ui.action.input > .ui.button, +.ui.action.input.error > .ui.button { + border-top: 1px solid var(--color-error-border); + border-bottom: 1px solid var(--color-error-border); +} + +.ui.action.input > .button, +.ui.action.input > .buttons { + display: flex; + align-items: center; + flex: 0 0 auto; +} +.ui.action.input > .button, +.ui.action.input > .buttons > .button { + padding-top: 0.78571429em; + padding-bottom: 0.78571429em; + margin: 0; +} + +.ui.action.input:not([class*="left action"]) > input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right-color: transparent; +} + +.ui.action.input > .dropdown:first-child, +.ui.action.input > .button:first-child, +.ui.action.input > .buttons:first-child > .button { + border-radius: 0.28571429rem 0 0 0.28571429rem; +} +.ui.action.input > .dropdown:not(:first-child), +.ui.action.input > .button:not(:first-child), +.ui.action.input > .buttons:not(:first-child) > .button { + border-radius: 0; +} +.ui.action.input > .dropdown:last-child, +.ui.action.input > .button:last-child, +.ui.action.input > .buttons:last-child > .button { + border-radius: 0 0.28571429rem 0.28571429rem 0; +} + +.ui.fluid.input { + display: flex; +} +.ui.fluid.input > input { + width: 0 !important; +} + +.ui.tiny.input { + font-size: 0.85714286em; +} +.ui.small.input { + font-size: 0.92857143em; +} + +.ui.action.input .ui.ui.button { + border-color: var(--color-input-border); + padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */ + padding-bottom: 0; +} + +/* currently used for search bar dropdowns in repo search and explore code */ +.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection { + min-width: 10em; +} +.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) { + border-right: none; +} +.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover { + border-color: var(--color-input-border); +} +.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} +.ui.action.input:not([class*="left action"]) > input, +.ui.action.input:not([class*="left action"]) > input:hover { + border-right: none; +} +.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection, +.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover, +.ui.action.input:not([class*="left action"]) > input:focus + .button, +.ui.action.input:not([class*="left action"]) > input:focus + .button:hover, +.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button, +.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover { + border-left-color: var(--color-primary); +} +.ui.action.input:not([class*="left action"]) > input:focus { + border-right-color: var(--color-primary); +} diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css new file mode 100644 index 0000000000..0512c5fddb --- /dev/null +++ b/web_src/css/modules/label.css @@ -0,0 +1,294 @@ +/* based on Fomantic UI label module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.label { + display: inline-flex; + align-items: center; + gap: .25rem; + vertical-align: middle; + line-height: 1; + background: var(--color-label-bg); + color: var(--color-label-text); + padding: 0.3em 0.5em; + text-transform: none; + font-size: 0.85714286rem; + font-weight: var(--font-weight-medium); + border: 0 solid transparent; + border-radius: 0.28571429rem; + white-space: nowrap; +} + +.ui.label:first-child { + margin-left: 0; +} +.ui.label:last-child { + margin-right: 0; +} + +a.ui.label { + cursor: pointer; +} + +.ui.label > a { + cursor: pointer; + color: inherit; + opacity: 0.75; +} +.ui.label > a:hover { + opacity: 1; +} + +.ui.label > img { + width: auto; + vertical-align: middle; + height: 2.1666em; +} + +.ui.label > .color-icon { + margin-left: 0; +} + +.ui.label > .icon { + width: auto; + margin: 0 0.75em 0 0; +} + +.ui.label > .detail { + display: inline-block; + vertical-align: top; + font-weight: var(--font-weight-medium); + margin-left: 1em; + opacity: 0.8; +} +.ui.label > .detail .icon { + margin: 0 0.25em 0 0; +} + +.ui.label > .close.icon, +.ui.label > .delete.icon { + cursor: pointer; + font-size: 0.92857143em; + opacity: 0.5; +} +.ui.label > .close.icon:hover, +.ui.label > .delete.icon:hover { + opacity: 1; +} + +.ui.label.left.icon > .close.icon, +.ui.label.left.icon > .delete.icon { + margin: 0 0.5em 0 0; +} +.ui.label:not(.icon) > .close.icon, +.ui.label:not(.icon) > .delete.icon { + margin: 0 0 0 0.5em; +} + +.ui.header > .ui.label { + margin-top: -0.29165em; +} + +a.ui.label:hover { + background: var(--color-label-hover-bg); + border-color: var(--color-label-hover-bg); + color: var(--color-label-text); +} + +.ui.label.visible:not(.dropdown) { + display: inline-block !important; +} + +.ui.basic.label { + background: var(--color-button); + border: 1px solid var(--color-light-border); + color: var(--color-text-light); + padding: calc(0.5833em - 1px) calc(0.833em - 1px); +} +a.ui.basic.label:hover { + text-decoration: none; + color: var(--color-text); + border-color: var(--color-light-border); + background: var(--color-hover); +} + +.ui.ui.ui.primary.label { + background: var(--color-primary); + border-color: var(--color-primary-dark-2); + color: var(--color-primary-contrast); +} +a.ui.ui.ui.primary.label:hover { + background: var(--color-primary-dark-3); + border-color: var(--color-primary-dark-3); + color: var(--color-primary-contrast); +} +.ui.ui.ui.basic.primary.label { + background: transparent; + border-color: var(--color-primary); + color: var(--color-primary); +} +a.ui.ui.ui.basic.primary.label:hover { + background: var(--color-hover); + border-color: var(--color-primary-dark-1); + color: var(--color-primary-dark-1); +} + +.ui.ui.ui.red.label { + background: var(--color-red); + border-color: var(--color-red); + color: var(--color-white); +} +a.ui.ui.ui.red.label:hover { + background: var(--color-red-dark-1); + border-color: var(--color-red-dark-1); + color: var(--color-white); +} +.ui.ui.ui.basic.red.label { + background: transparent; + border-color: var(--color-red); + color: var(--color-red); +} +a.ui.ui.ui.basic.red.label:hover { + background: transparent; + border-color: var(--color-red-dark-1); + color: var(--color-red-dark-1); +} + +.ui.ui.ui.orange.label { + background: var(--color-orange); + border-color: var(--color-orange); + color: var(--color-white); +} +a.ui.ui.ui.orange.label:hover { + background: var(--color-orange-dark-1); + border-color: var(--color-orange-dark-1); + color: var(--color-white); +} +.ui.ui.ui.basic.orange.label { + background: transparent; + border-color: var(--color-orange); + color: var(--color-orange); +} +a.ui.ui.ui.basic.orange.label:hover { + background: transparent; + border-color: var(--color-orange-dark-1); + color: var(--color-orange-dark-1); +} + +.ui.ui.ui.yellow.label { + background: var(--color-yellow); + border-color: var(--color-yellow); + color: var(--color-white); +} +a.ui.ui.ui.yellow.label:hover { + background: var(--color-yellow-dark-1); + border-color: var(--color-yellow-dark-1); + color: var(--color-white); +} +.ui.ui.ui.basic.yellow.label { + background: transparent; + border-color: var(--color-yellow); + color: var(--color-yellow); +} +a.ui.ui.ui.basic.yellow.label:hover { + background: transparent; + border-color: var(--color-yellow-dark-1); + color: var(--color-yellow-dark-1); +} +.ui.ui.ui.olive.label { + background: var(--color-olive); + border-color: var(--color-olive); + color: var(--color-white); +} + +.ui.ui.ui.green.label { + background: var(--color-green); + border-color: var(--color-green); + color: var(--color-white); +} +a.ui.ui.ui.green.label:hover { + background: var(--color-green-dark-1); + border-color: var(--color-green-dark-1); + color: var(--color-white); +} +.ui.ui.ui.basic.green.label { + background: transparent; + border-color: var(--color-green); + color: var(--color-green); +} +a.ui.ui.ui.basic.green.label:hover { + background: transparent; + border-color: var(--color-green-dark-1); + color: var(--color-green-dark-1); +} + +.ui.ui.ui.purple.label { + background: var(--color-purple); + border-color: var(--color-purple); + color: var(--color-white); +} +a.ui.ui.ui.purple.label:hover { + background: var(--color-purple-dark-1); + border-color: var(--color-purple-dark-1); + color: var(--color-white); +} +.ui.ui.ui.basic.purple.label { + background: transparent; + border-color: var(--color-purple); + color: var(--color-purple); +} +a.ui.ui.ui.basic.purple.label:hover { + background: transparent; + border-color: var(--color-purple-dark-1); + color: var(--color-purple-dark-1); +} + +.ui.ui.ui.grey.label { + background: var(--color-label-bg); + border-color: var(--color-label-bg); + color: var(--color-label-text); +} +a.ui.ui.ui.grey.label:hover { + background: var(--color-label-hover-bg); + border-color: var(--color-label-hover-bg); + color: var(--color-white); +} +.ui.ui.ui.basic.grey.label { + background: transparent; + border-color: var(--color-label-bg); + color: var(--color-label-text); +} +a.ui.ui.ui.basic.grey.label:hover { + background: transparent; + border-color: var(--color-label-hover-bg); + color: var(--color-label-hover-bg); +} + +.ui.horizontal.label { + margin: 0 0.5em 0 0; + padding: 0.4em 0.833em; + min-width: 3em; + text-align: center; +} + +.ui.circular.label { + min-width: 2em; + min-height: 2em; + padding: 0.5em !important; + line-height: 1; + text-align: center; + border-radius: 500rem; + justify-content: center; +} + +.ui.mini.label { + font-size: 0.64285714rem; +} +.ui.tiny.label { + font-size: 0.71428571rem; +} +.ui.small.label { + font-size: 0.78571429rem; +} +.ui.large.label { + font-size: 1rem; +} diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css new file mode 100644 index 0000000000..32c71e802b --- /dev/null +++ b/web_src/css/modules/list.css @@ -0,0 +1,193 @@ +/* based on Fomantic UI list module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.list { + list-style-type: none; + margin: 1em 0; + padding: 0; + font-size: 1em; +} + +.ui.list:first-child { + margin-top: 0; + padding-top: 0; +} + +.ui.list:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +.ui.list > .item, +.ui.list .list > .item { + display: list-item; + table-layout: fixed; + list-style-type: none; + list-style-position: outside; +} + +.ui.list > .list > .item::after, +.ui.list > .item::after { + content: ""; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.ui.list .list:not(.icon) { + clear: both; + margin: 0; + padding: 0.75em 0 0.25em 0.5em; +} + +.ui.list .list > .item { + padding: 0.14285714em 0; +} + +.ui.list .list > .item > i.icon, +.ui.list > .item > i.icon { + display: table-cell; + min-width: 1.55em; + padding-top: 0; + transition: color 0.1s ease; + padding-right: 0.28571429em; + vertical-align: top; +} +.ui.list .list > .item > i.icon:only-child, +.ui.list > .item > i.icon:only-child { + display: inline-block; + min-width: auto; + vertical-align: top; +} + +.ui.list .list > .item > .image, +.ui.list > .item > .image { + display: table-cell; + background-color: transparent; + vertical-align: top; +} +.ui.list .list > .item > .image:not(:only-child):not(img), +.ui.list > .item > .image:not(:only-child):not(img) { + padding-right: 0.5em; +} +.ui.list .list > .item > .image img, +.ui.list > .item > .image img { + vertical-align: top; +} +.ui.list .list > .item > img.image, +.ui.list .list > .item > .image:only-child, +.ui.list > .item > img.image, +.ui.list > .item > .image:only-child { + display: inline-block; +} + +.ui.list .list > .item > .content, +.ui.list > .item > .content { + color: var(--color-text); +} +.ui.list .list > .item > .image + .content, +.ui.list .list > .item > i.icon + .content, +.ui.list > .item > .image + .content, +.ui.list > .item > i.icon + .content { + display: table-cell; + width: 100%; + padding: 0 0 0 0.5em; + vertical-align: top; +} +.ui.list .list > .item > img.image + .content, +.ui.list > .item > img.image + .content { + display: inline-block; + width: auto; +} +.ui.list .list > .item > .content > .list, +.ui.list > .item > .content > .list { + margin-left: 0; + padding-left: 0; +} + +.ui.list .list > .item .header, +.ui.list > .item .header { + display: block; + margin: 0; + font-family: var(--fonts-regular); + font-weight: var(--font-weight-medium); + color: var(--color-text-dark); +} + +.ui.list .list > .item .description, +.ui.list > .item .description { + display: block; + color: var(--color-text); +} + +.ui.list > .item a, +.ui.list .list > .item a { + cursor: pointer; +} + +.ui.list .list > .item [class*="right floated"], +.ui.list > .item [class*="right floated"] { + float: right; + margin: 0 0 0 1em; +} + +.ui.menu .ui.list > .item, +.ui.menu .ui.list .list > .item { + display: list-item; + table-layout: fixed; + background-color: transparent; + list-style-type: none; + list-style-position: outside; + padding: 0.21428571em 0; +} +.ui.menu .ui.list .list > .item::before, +.ui.menu .ui.list > .item::before { + border: none; + background: none; +} +.ui.menu .ui.list .list > .item:first-child, +.ui.menu .ui.list > .item:first-child { + padding-top: 0; +} +.ui.menu .ui.list .list > .item:last-child, +.ui.menu .ui.list > .item:last-child { + padding-bottom: 0; +} + +.ui.list .list > .disabled.item, +.ui.list > .disabled.item { + pointer-events: none; + opacity: var(--opacity-disabled); +} + +.ui.list .list > a.item:hover > .icons, +.ui.list > a.item:hover > .icons, +.ui.list .list > a.item:hover > i.icon, +.ui.list > a.item:hover > i.icon { + color: var(--color-text-dark); +} + +.ui.divided.list > .item { + border-top: 1px solid var(--color-secondary); +} +.ui.divided.list .list > .item { + border-top: none; +} +.ui.divided.list .item .list > .item { + border-top: none; +} +.ui.divided.list .list > .item:first-child, +.ui.divided.list > .item:first-child { + border-top: none; +} +.ui.divided.list .list > .item:first-child { + border-top-width: 1px; +} + +.ui.relaxed.list > .item:not(:first-child) { + padding-top: 0.42857143em; +} +.ui.relaxed.list > .item:not(:last-child) { + padding-bottom: 0.42857143em; +} diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css new file mode 100644 index 0000000000..c62dbddd25 --- /dev/null +++ b/web_src/css/modules/message.css @@ -0,0 +1,114 @@ +/* based on Fomantic UI message module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.message { + background: var(--color-box-body); + color: var(--color-text); + border: 1px solid var(--color-secondary); + position: relative; + min-height: 1em; + margin: 1em 0; + padding: 1em 1.5em; + border-radius: var(--border-radius); +} + +.ui.message:first-child { + margin-top: 0; +} + +.ui.message:last-child { + margin-bottom: 0; +} + +.ui.attached.message { + margin-bottom: -1px; + border-radius: var(--border-radius) var(--border-radius) 0 0; + margin-left: -1px; + margin-right: -1px; +} + +.ui.attached + .ui.attached.message:not(.top):not(.bottom) { + margin-top: -1px; + border-radius: 0; +} + +.ui.bottom.attached.message { + margin-top: -1px; + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +.ui.bottom.attached.message:not(:last-child) { + margin-bottom: 1em; +} + +.ui.info.message .header, +.ui.blue.message .header { + color: var(--color-blue); +} + +.ui.info.message, +.ui.attached.info.message, +.ui.blue.message, +.ui.attached.blue.message { + background: var(--color-info-bg); + color: var(--color-info-text); + border-color: var(--color-info-border); +} + +.ui.success.message .header, +.ui.positive.message .header, +.ui.green.message .header { + color: var(--color-green); +} + +.ui.success.message, +.ui.attached.success.message, +.ui.positive.message, +.ui.attached.positive.message { + background: var(--color-success-bg); + color: var(--color-success-text); + border-color: var(--color-success-border); +} + +.ui.error.message .header, +.ui.negative.message .header, +.ui.red.message .header { + color: var(--color-red); +} + +.ui.error.message, +.ui.attached.error.message, +.ui.red.message, +.ui.attached.red.message, +.ui.negative.message, +.ui.attached.negative.message { + background: var(--color-error-bg); + color: var(--color-error-text); + border-color: var(--color-error-border); +} + +.ui.warning.message .header, +.ui.yellow.message .header { + color: var(--color-yellow); +} + +.ui.warning.message, +.ui.attached.warning.message, +.ui.yellow.message, +.ui.attached.yellow.message { + background: var(--color-warning-bg); + color: var(--color-warning-text); + border-color: var(--color-warning-border); +} + +.ui.message > .close.icon { + cursor: pointer; + position: absolute; + top: 9px; + right: 9px; + opacity: .7; +} + +.ui.message > .close.icon:hover { + opacity: 1; +} diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css index b6fd2ff20a..d7aa197e02 100644 --- a/web_src/css/modules/navbar.css +++ b/web_src/css/modules/navbar.css @@ -108,13 +108,6 @@ } } -@media (min-width: 767.98px) { - #navbar .navbar-mobile-right, - #navbar .mobile-only { - display: none; - } -} - #navbar a.item .notification_count { color: var(--color-nav-bg); padding: 0 3.75px; @@ -143,3 +136,12 @@ justify-content: center; z-index: 1; /* prevent menu button background from overlaying icon */ } + +.secondary-nav { + background: var(--color-secondary-nav-bg) !important; /* important because of .ui.secondary.menu */ +} + +.issue-navbar { + display: flex; + justify-content: space-between; +} diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css new file mode 100644 index 0000000000..bbd39c385f --- /dev/null +++ b/web_src/css/modules/segment.css @@ -0,0 +1,196 @@ +/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.segment { + position: relative; + margin: 1rem 0; + padding: 1em; + border-radius: 0.28571429rem; + border: 1px solid var(--color-secondary); + background: var(--color-box-body); + color: var(--color-text); +} +.ui.segment:first-child { + margin-top: 0; +} +.ui.segment:last-child { + margin-bottom: 0; +} + +.ui.grid.segment { + margin: 1rem 0; + border-radius: 0.28571429rem; +} + +.ui.segment.tab:last-child { + margin-bottom: 1rem; +} + +.ui.segments { + flex-direction: column; + position: relative; + margin: 1rem 0; + border: 1px solid var(--color-secondary); + border-radius: 0.28571429rem; + background: var(--color-box-body); + color: var(--color-text); +} +.ui.segments:first-child { + margin-top: 0; +} +.ui.segments:last-child { + margin-bottom: 0; +} + +.ui.segments > .segment { + top: 0; + bottom: 0; + border-radius: 0; + margin: 0; + width: auto; + box-shadow: none; + border: none; + border-top: 1px solid var(--color-secondary); +} +.ui.segments:not(.horizontal) > .segment:first-child { + top: 0; + bottom: 0; + border-top: none; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.28571429rem 0.28571429rem 0 0; +} + +.ui.segments:not(.horizontal) > .segment:last-child { + top: 0; + bottom: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0 0 0.28571429rem 0.28571429rem; +} + +.ui.segments:not(.horizontal) > .segment:only-child, +.ui.segments:not(.horizontal) > .segment:has(~ .tw-hidden) { /* workaround issue with :last-child ignoring hidden elements */ + border-radius: 0.28571429rem; +} + +.ui.segments > .ui.segments { + border-top: 1px solid var(--color-secondary); + margin: 1rem; +} +.ui.segments > .segments:first-child { + border-top: none; +} +.ui.segments > .segment + .segments:not(.horizontal) { + margin-top: 0; +} + +.ui.horizontal.segments { + display: flex; + flex-direction: row; + background-color: transparent; + padding: 0; + margin: 1rem 0; + border-radius: 0.28571429rem; + border: 1px solid var(--color-secondary); +} + +.ui.horizontal.segments > .segment { + margin: 0; + min-width: 0; + border-radius: 0; + border: none; + box-shadow: none; + border-left: 1px solid var(--color-secondary); +} + +.ui.segments > .horizontal.segments:first-child { + border-top: none; +} +.ui.horizontal.segments:not(.stackable) > .segment:first-child { + border-left: none; +} +.ui.horizontal.segments > .segment:first-child { + border-radius: 0.28571429rem 0 0 0.28571429rem; +} +.ui.horizontal.segments > .segment:last-child { + border-radius: 0 0.28571429rem 0.28571429rem 0; +} + +.ui.clearing.segment::after { + content: ""; + display: block; + clear: both; +} + +.ui[class*="left aligned"].segment { + text-align: left; +} +.ui[class*="center aligned"].segment { + text-align: center; +} + +.ui.secondary.segment { + background: var(--color-secondary-bg); + color: var(--color-text-light); +} + +.ui.attached.segment { + top: 0; + bottom: 0; + border-radius: 0; + margin: 0 -1px; + width: calc(100% + 2px); + max-width: calc(100% + 2px); + box-shadow: none; + border: 1px solid var(--color-secondary); + background: var(--color-box-body); + color: var(--color-text); +} +.ui.attached:not(.message) + .ui.attached.segment:not(.top) { + border-top: none; +} + +.ui[class*="top attached"].segment { + bottom: 0; + margin-bottom: 0; + top: 0; + margin-top: 1rem; + border-radius: 0.28571429rem 0.28571429rem 0 0; +} +.ui.segment[class*="top attached"]:first-child { + margin-top: 0; +} + +.ui.segment[class*="bottom attached"] { + bottom: 0; + margin-top: 0; + top: 0; + margin-bottom: 1rem; + border-radius: 0 0 0.28571429rem 0.28571429rem; +} +.ui.segment[class*="bottom attached"]:last-child { + margin-bottom: 1rem; +} + +.ui.fitted.segment:not(.horizontally) { + padding-top: 0; + padding-bottom: 0; +} +.ui.fitted.segment:not(.vertically) { + padding-left: 0; + padding-right: 0; +} + +.ui.segments .segment, +.ui.segment { + font-size: 1rem; +} + +.ui.error.segment { + border-color: var(--color-error-border) !important; +} + +.ui.warning.segment { + border-color: var(--color-warning-border) !important; +} diff --git a/web_src/css/modules/table.css b/web_src/css/modules/table.css new file mode 100644 index 0000000000..4fb9d4214e --- /dev/null +++ b/web_src/css/modules/table.css @@ -0,0 +1,385 @@ +/* based on Fomantic UI segment module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.table { + width: 100%; + margin: 1em 0; + border: 1px solid var(--color-secondary); + border-radius: 0.28571429rem; + vertical-align: middle; + border-collapse: separate; + border-spacing: 0; + color: var(--color-text); + background: var(--color-box-body); + border-color: var(--color-secondary); + text-align: start; +} + +.ui.table:first-child { + margin-top: 0; +} +.ui.table:last-child { + margin-bottom: 0; +} +.ui.table > thead, +.ui.table > tbody { + text-align: inherit; + vertical-align: inherit; +} + +.ui.table > thead > tr > th { + background: var(--color-box-header); + text-align: inherit; + color: var(--color-text); + padding: 6px 5px; + vertical-align: inherit; + font-weight: var(--font-weight-normal); + border-bottom: 1px solid var(--color-secondary); + border-left: none; +} +.ui.table > thead > tr > th:first-child { + border-left: none; +} +.ui.table > thead > tr:first-child > th:first-child { + border-radius: 0.28571429rem 0 0; +} +.ui.table > thead > tr:first-child > th:last-child { + border-radius: 0 0.28571429rem 0 0; +} +.ui.table > thead > tr:first-child > th:only-child { + border-radius: 0.28571429rem 0.28571429rem 0 0; +} + +.ui.table > tfoot > tr > th, +.ui.table > tfoot > tr > td { + border-top: 1px solid var(--color-secondary); + background: var(--color-box-body); + text-align: inherit; + color: var(--color-text); + padding: 0.78571429em; + vertical-align: inherit; + font-weight: var(--font-weight-normal); +} +.ui.table > tfoot > tr > th:first-child, +.ui.table > tfoot > tr > td:first-child { + border-left: none; +} +.ui.table > tfoot > tr:first-child > th:first-child, +.ui.table > tfoot > tr:first-child > td:first-child { + border-radius: 0 0 0 0.28571429rem; +} +.ui.table > tfoot > tr:first-child > th:last-child, +.ui.table > tfoot > tr:first-child > td:last-child { + border-radius: 0 0 0.28571429rem; +} +.ui.table > tfoot > tr:first-child > th:only-child, +.ui.table > tfoot > tr:first-child > td:only-child { + border-radius: 0 0 0.28571429rem 0.28571429rem; +} + +.ui.table > tr > td, +.ui.table > tbody > tr > td { + border-top: 1px solid var(--color-secondary-alpha-50); + padding: 6px 5px; + text-align: inherit; +} +.ui.table > tr:first-child > td, +.ui.table > tbody > tr:first-child > td { + border-top: none; +} + +.ui.table.segment { + padding: 0; +} +.ui.table.segment::after { + display: none; +} + +@media only screen and (max-width: 767.98px) { + .ui.table:not(.unstackable) { + width: 100%; + padding: 0; + } + .ui.table:not(.unstackable) > thead, + .ui.table:not(.unstackable) > thead > tr, + .ui.table:not(.unstackable) > tfoot, + .ui.table:not(.unstackable) > tfoot > tr, + .ui.table:not(.unstackable) > tbody, + .ui.table:not(.unstackable) > tr, + .ui.table:not(.unstackable) > tbody > tr, + .ui.table:not(.unstackable) > tr > th, + .ui.table:not(.unstackable) > thead > tr > th, + .ui.table:not(.unstackable) > tbody > tr > th, + .ui.table:not(.unstackable) > tfoot > tr > th, + .ui.table:not(.unstackable) > tr > td, + .ui.table:not(.unstackable) > tbody > tr > td, + .ui.table:not(.unstackable) > tfoot > tr > td { + display: block !important; + width: auto !important; + } + .ui.table:not(.unstackable) > thead { + display: block; + } + .ui.table:not(.unstackable) > tfoot { + display: block; + } + .ui.ui.ui.ui.table:not(.unstackable) > tr, + .ui.ui.ui.ui.table:not(.unstackable) > thead > tr, + .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr, + .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr { + padding-top: 1em; + padding-bottom: 1em; + } + .ui.ui.ui.ui.table:not(.unstackable) > tr > th, + .ui.ui.ui.ui.table:not(.unstackable) > thead > tr > th, + .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > th, + .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > th, + .ui.ui.ui.ui.table:not(.unstackable) > tr > td, + .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > td, + .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > td { + background: none; + border: none; + padding: 0.25em 0.75em; + } + .ui.table:not(.unstackable) > tr > th:first-child, + .ui.table:not(.unstackable) > thead > tr > th:first-child, + .ui.table:not(.unstackable) > tbody > tr > th:first-child, + .ui.table:not(.unstackable) > tfoot > tr > th:first-child, + .ui.table:not(.unstackable) > tr > td:first-child, + .ui.table:not(.unstackable) > tbody > tr > td:first-child, + .ui.table:not(.unstackable) > tfoot > tr > td:first-child { + font-weight: var(--font-weight-normal); + } +} + +.ui.table[class*="left aligned"], +.ui.table [class*="left aligned"] { + text-align: left; +} + +.ui.table[class*="center aligned"], +.ui.table [class*="center aligned"] { + text-align: center; +} + +.ui.table[class*="right aligned"], +.ui.table [class*="right aligned"] { + text-align: right; +} + +.ui.table[class*="top aligned"], +.ui.table [class*="top aligned"] { + vertical-align: top; +} + +.ui.table[class*="middle aligned"], +.ui.table [class*="middle aligned"] { + vertical-align: middle; +} + +.ui.table th.collapsing, +.ui.table td.collapsing { + width: 1px; + white-space: nowrap; +} + +.ui.fixed.table { + table-layout: fixed; +} +.ui.fixed.table th, +.ui.fixed.table td { + overflow: hidden; + text-overflow: ellipsis; +} + +.ui.attached.table { + top: 0; + bottom: 0; + border-radius: 0; + margin: 0 -1px; + width: calc(100% + 2px); + max-width: calc(100% + 2px); + border: 1px solid var(--color-secondary); +} +.ui.attached + .ui.attached.table:not(.top) { + border-top: none; +} + +.ui[class*="bottom attached"].table { + bottom: 0; + margin-top: 0; + top: 0; + margin-bottom: 1em; + border-radius: 0 0 0.28571429rem 0.28571429rem; +} +.ui[class*="bottom attached"].table:last-child { + margin-bottom: 0; +} + +.ui.striped.table > tr:nth-child(2n), +.ui.striped.table > tbody > tr:nth-child(2n) { + background: var(--color-light); +} + +.ui.table[class*="single line"], +.ui.table [class*="single line"] { + white-space: nowrap; +} + +/* Column Width */ +.ui.table th.one.wide, +.ui.table td.one.wide { + width: 6.25%; +} +.ui.table th.two.wide, +.ui.table td.two.wide { + width: 12.5%; +} +.ui.table th.three.wide, +.ui.table td.three.wide { + width: 18.75%; +} +.ui.table th.four.wide, +.ui.table td.four.wide { + width: 25%; +} +.ui.table th.five.wide, +.ui.table td.five.wide { + width: 31.25%; +} +.ui.table th.six.wide, +.ui.table td.six.wide { + width: 37.5%; +} +.ui.table th.seven.wide, +.ui.table td.seven.wide { + width: 43.75%; +} +.ui.table th.eight.wide, +.ui.table td.eight.wide { + width: 50%; +} +.ui.table th.nine.wide, +.ui.table td.nine.wide { + width: 56.25%; +} +.ui.table th.ten.wide, +.ui.table td.ten.wide { + width: 62.5%; +} +.ui.table th.eleven.wide, +.ui.table td.eleven.wide { + width: 68.75%; +} +.ui.table th.twelve.wide, +.ui.table td.twelve.wide { + width: 75%; +} +.ui.table th.thirteen.wide, +.ui.table td.thirteen.wide { + width: 81.25%; +} +.ui.table th.fourteen.wide, +.ui.table td.fourteen.wide { + width: 87.5%; +} +.ui.table th.fifteen.wide, +.ui.table td.fifteen.wide { + width: 93.75%; +} +.ui.table th.sixteen.wide, +.ui.table td.sixteen.wide { + width: 100%; +} + +.ui.basic.table { + background: transparent; + border: 1px solid var(--color-secondary); +} +.ui.basic.table > thead > tr > th, +.ui.basic.table > tbody > tr > th, +.ui.basic.table > tfoot > tr > th, +.ui.basic.table > tr > th { + background: transparent; + border-left: none; +} +.ui.basic.table > tbody > tr { + border-bottom: 1px solid var(--color-secondary); +} +.ui.basic.table > tbody > tr > td, +.ui.basic.table > tfoot > tr > td, +.ui.basic.table > tr > td { + background: transparent; +} +.ui.basic.striped.table > tbody > tr:nth-child(2n) { + background: var(--color-light); +} + +.ui[class*="very basic"].table { + border: none; +} +.ui[class*="very basic"].table:not(.striped) > tr > th:first-child, +.ui[class*="very basic"].table:not(.striped) > thead > tr > th:first-child, +.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:first-child, +.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:first-child, +.ui[class*="very basic"].table:not(.striped) > tr > td:first-child, +.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:first-child, +.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:first-child { + padding-left: 0; +} +.ui[class*="very basic"].table:not(.striped) > tr > th:last-child, +.ui[class*="very basic"].table:not(.striped) > thead > tr > th:last-child, +.ui[class*="very basic"].table:not(.striped) > tbody > tr > th:last-child, +.ui[class*="very basic"].table:not(.striped) > tfoot > tr > th:last-child, +.ui[class*="very basic"].table:not(.striped) > tr > td:last-child, +.ui[class*="very basic"].table:not(.striped) > tbody > tr > td:last-child, +.ui[class*="very basic"].table:not(.striped) > tfoot > tr > td:last-child { + padding-right: 0; +} +.ui[class*="very basic"].table:not(.striped) > thead > tr:first-child > th { + padding-top: 0; +} + +.ui.celled.table > tr > th, +.ui.celled.table > thead > tr > th, +.ui.celled.table > tbody > tr > th, +.ui.celled.table > tfoot > tr > th, +.ui.celled.table > tr > td, +.ui.celled.table > tbody > tr > td, +.ui.celled.table > tfoot > tr > td { + border-left: 1px solid var(--color-secondary-alpha-50); +} +.ui.celled.table > tr > th:first-child, +.ui.celled.table > thead > tr > th:first-child, +.ui.celled.table > tbody > tr > th:first-child, +.ui.celled.table > tfoot > tr > th:first-child, +.ui.celled.table > tr > td:first-child, +.ui.celled.table > tbody > tr > td:first-child, +.ui.celled.table > tfoot > tr > td:first-child { + border-left: none; +} + +.ui.compact.table > tr > th, +.ui.compact.table > thead > tr > th, +.ui.compact.table > tbody > tr > th, +.ui.compact.table > tfoot > tr > th { + padding-left: 0.7em; + padding-right: 0.7em; +} +.ui.compact.table > tr > td, +.ui.compact.table > tbody > tr > td, +.ui.compact.table > tfoot > tr > td { + padding: 0.5em 0.7em; +} + +/* use more horizontal padding on first and last items for visuals */ +.ui.table > thead > tr > th:first-of-type, +.ui.table > tbody > tr > td:first-of-type, +.ui.table > tr > td:first-of-type { + padding-left: 10px; +} +.ui.table > thead > tr > th:last-of-type, +.ui.table > tbody > tr > td:last-of-type, +.ui.table > tr > td:last-of-type { + padding-right: 10px; +} diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css index d65ecc89fb..6ac7c37d93 100644 --- a/web_src/css/modules/tippy.css +++ b/web_src/css/modules/tippy.css @@ -5,6 +5,11 @@ display: none !important; } +/* show target element once it's been moved by tippy.js */ +.tippy-content .tippy-target { + display: unset !important; +} + [data-tippy-root] { max-width: calc(100vw - 32px); } @@ -24,6 +29,17 @@ z-index: 1; } +/* bare theme, no styling at all, except box-shadow */ +.tippy-box[data-theme="bare"] { + border: none; + box-shadow: 0 6px 18px var(--color-shadow); +} + +.tippy-box[data-theme="bare"] .tippy-content { + padding: 0; + background: transparent; +} + /* tooltip theme for text tooltips */ .tippy-box[data-theme="tooltip"] { @@ -46,18 +62,40 @@ .tippy-box[data-theme="menu"] { background-color: var(--color-menu); color: var(--color-text); + box-shadow: 0 6px 18px var(--color-shadow); } .tippy-box[data-theme="menu"] .tippy-content { - padding: 0; + padding: 4px 0; } .tippy-box[data-theme="menu"] .tippy-svg-arrow-inner { fill: var(--color-menu); } +.tippy-box[data-theme="menu"] .item { + display: flex; + align-items: center; + padding: 9px 18px; + color: inherit; + text-decoration: none; + gap: 10px; +} + +.tippy-box[data-theme="menu"] .item:hover { + background: var(--color-hover); +} + +.tippy-box[data-theme="menu"] .item:focus { + background: var(--color-active); +} + /* box-with-header theme to look like .ui.attached.segment. can contain .ui.attached.header */ +.tippy-box[data-theme="box-with-header"] { + box-shadow: 0 6px 18px var(--color-shadow); +} + .tippy-box[data-theme="box-with-header"] .tippy-content { background: var(--color-box-body); border-radius: var(--border-radius); diff --git a/web_src/css/org.css b/web_src/css/org.css index d2bf0ff606..32e8a914fa 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -89,50 +89,44 @@ text-align: center; } -.organization.options input { - min-width: 300px; -} - -.organization.profile .org-avatar { - width: 100px; - height: 100px; +.page-content.organization .org-avatar { margin-right: 15px; } -.organization.profile #org-info { +.page-content.organization #org-info { overflow-wrap: anywhere; flex: 1; word-break: break-all; } -.organization.profile #org-info .ui.header { +.page-content.organization #org-info .ui.header { display: flex; align-items: center; font-size: 36px; margin-bottom: 0; } -.organization.profile #org-info .desc { +.page-content.organization #org-info .desc { font-size: 16px; margin-bottom: 10px; } -.organization.profile #org-info .meta { +.page-content.organization #org-info .meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } -.organization.profile .ui.top.header .ui.right { +.page-content.organization .ui.top.header .ui.right { margin-top: 0; } -.organization.profile .teams .item { +.page-content.organization .teams .item { padding: 10px 15px; } -.organization.profile .members .ui.avatar { +.page-content.organization .members .ui.avatar { width: 48px; height: 48px; margin-right: 5px; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 55c6ec4817..c579745238 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -4,16 +4,6 @@ user-select: none; } -.repository .navbar { - display: flex; - justify-content: space-between; -} - -.repository .navbar .ui.label { - margin-left: 7px; - padding: 3px 5px; -} - .repository .owner.dropdown { min-width: 40% !important; } @@ -143,41 +133,31 @@ margin-bottom: 12px; } -.repository #clone-panel #repo-clone-url { +.repository .clone-panel #repo-clone-url { width: 320px; border-radius: 0; } -@media (min-width: 768px) and (max-width: 991.98px) { - .repository #clone-panel #repo-clone-url { +@media (max-width: 991.98px) { + .repository .clone-panel #repo-clone-url { width: 200px; } } -@media (max-width: 767.98px) { - .repository #clone-panel #repo-clone-url { - width: 200px; - } +.repository .ui.action.input.clone-panel > button + button, +.repository .ui.action.input.clone-panel > button + input { + margin-left: -1px; /* make the borders overlap to avoid double borders */ } -.repository #clone-panel #repo-clone-https, -.repository #clone-panel #repo-clone-ssh { - border-right: none; -} - -.repository #clone-panel #more-btn { - border-left: none; -} - -.repository #clone-panel button:first-of-type { +.repository .clone-panel > button:first-of-type { border-radius: var(--border-radius) 0 0 var(--border-radius) !important; } -.repository #clone-panel button:last-of-type { +.repository .clone-panel > button:last-of-type { border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; } -.repository #clone-panel .dropdown .menu { +.repository .clone-panel .dropdown .menu { right: 0 !important; left: auto !important; } @@ -197,12 +177,44 @@ } } -.repository.file.list .repo-path { - word-break: break-word; +.commit-summary { + flex: 1; + overflow-wrap: anywhere; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } -.repository.file.list #repo-files-table { - table-layout: fixed; +.commit-header .commit-summary, +td .commit-summary { + white-space: normal; +} + +.latest-commit { + display: flex; + flex: 1; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 767.98px) { + .latest-commit .sha { + display: none; + } + .latest-commit .commit-summary { + margin-left: 8px; + } +} + +.repo-path { + display: flex; + overflow-wrap: anywhere; +} + +/* this is what limits the commit table width to a value that works on all viewport sizes */ +#repo-files-table th:first-of-type { + max-width: calc(calc(min(100vw, 1280px)) - 145px - calc(2 * var(--page-margin-x))); } .repository.file.list #repo-files-table thead th { @@ -282,7 +294,6 @@ } .repository.file.list #repo-files-table td.age { - width: 120px; color: var(--color-text-light-1); } @@ -935,18 +946,11 @@ .repository.view.issue .comment-list .comment .merge-section .item-section { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 0; - margin-top: -0.25rem; - margin-bottom: -0.25rem; -} - -@media (max-width: 767.98px) { - .repository.view.issue .comment-list .comment .merge-section .item-section { - align-items: flex-start; - flex-direction: column; - } + gap: 0.5em; } .repository.view.issue .comment-list .comment .merge-section .divider { @@ -1072,11 +1076,7 @@ .repository.view.issue .comment-list .event .detail { margin-top: 4px; - margin-left: 14px; -} - -.repository.view.issue .comment-list .event .detail .svg { - margin-right: 2px; + margin-left: 15px; } .repository.view.issue .comment-list .event .segments { @@ -1250,10 +1250,6 @@ margin: 0; } -.repository #commits-table td.message { - text-overflow: unset; -} - .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) { background-color: var(--color-light) !important; } @@ -1427,6 +1423,7 @@ .repository .data-table tr { border-top: 0; + background: none !important; } .repository .data-table td, @@ -1439,6 +1436,21 @@ border: 1px solid var(--color-secondary); } +/* the border css competes with .markup where all tables have outer border which would add a double + border here, remove only the outer borders from this table */ +.repository .data-table tr:first-child :is(td,th) { + border-top: none !important; +} +.repository .data-table tr:last-child :is(td,th) { + border-bottom: none !important; +} +.repository .data-table tr :is(td,th):first-child { + border-left: none !important; +} +.repository .data-table tr :is(td,th):last-child { + border-right: none !important; +} + .repository .data-table td { white-space: pre-line; } @@ -1476,7 +1488,7 @@ min-width: 50px; font-family: monospace; line-height: 20px; - color: var(--color-secondary-dark-2); + color: var(--color-text-light-1); white-space: nowrap; vertical-align: top; cursor: pointer; @@ -1498,12 +1510,6 @@ background: var(--color-body); } -@media (max-width: 991.98px) { - .repository .diff-detail-box { - flex-direction: row; - } -} - @media (max-width: 480px) { .repository .diff-detail-box { flex-wrap: wrap; @@ -1528,7 +1534,7 @@ color: var(--color-red); } -@media (max-width: 991.98px) { +@media (max-width: 800px) { .repository .diff-detail-box .diff-detail-stats { display: none !important; } @@ -1538,7 +1544,6 @@ display: flex; align-items: center; gap: 0.25em; - flex-wrap: wrap; justify-content: end; } @@ -1548,15 +1553,6 @@ margin-right: 0 !important; } -@media (max-width: 480px) { - .repository .diff-detail-box .diff-detail-actions { - padding-top: 0.25rem; - } - .repository .diff-detail-box .diff-detail-actions .ui.button:not(.btn-submit) { - padding: 0 0.5rem; - } -} - .repository .diff-detail-box span.status { display: inline-block; width: 12px; @@ -1633,7 +1629,6 @@ .repository .diff-file-box .file-body.file-code .lines-num { text-align: right; - color: var(--color-text-light); width: 1%; min-width: 50px; } @@ -1770,10 +1765,6 @@ font-weight: var(--font-weight-normal); } -.repository.quickstart .guide .clone.button:first-child { - border-radius: var(--border-radius) 0 0 var(--border-radius); -} - .repository.quickstart .guide #repo-clone-url { border-radius: 0; padding: 5px 10px; @@ -2046,13 +2037,8 @@ } #cite-repo-modal #citation-panel { - width: 500px; -} - -@media (max-width: 767.98px) { - #cite-repo-modal #citation-panel { - width: 100%; - } + display: flex; + width: 100%; } #cite-repo-modal #citation-panel input { @@ -2072,6 +2058,7 @@ padding: 5px 10px; font-size: 1.2em; line-height: 1.4; + flex: 1; } #cite-repo-modal #citation-panel #citation-copy-apa, @@ -2154,6 +2141,20 @@ padding-bottom: 0 !important; } +.commit-header-buttons { + display: flex; + gap: 4px; + align-items: flex-start; + white-space: nowrap; +} + +@media (max-width: 767.98px) { + .commit-header-buttons { + flex-direction: column; + align-items: stretch; + } +} + .settings.webhooks .list > .item:not(:first-child), .settings.githooks .list > .item:not(:first-child), .settings.actions .list > .item:not(:first-child) { @@ -2289,10 +2290,6 @@ padding: 1em; } -.comment-body .markup { - border-radius: 0 0 var(--border-radius) var(--border-radius); /* don't render outside box */ -} - .edit-label.modal .form .column, .new-label.modal .form .column { padding-right: 0; @@ -2304,104 +2301,6 @@ padding-top: 15px; } -.edit-label.modal .form .color.picker.column, -.new-label.modal .form .color.picker.column { - display: flex; -} - -.edit-label.modal .form .color.picker.column .minicolors, -.new-label.modal .form .color.picker.column .minicolors { - flex: 1; -} - -.edit-label.modal .form .minicolors-swatch.minicolors-sprite, -.new-label.modal .form .minicolors-swatch.minicolors-sprite { - top: 10px; - left: 10px; - width: 15px; - height: 15px; -} - -.tab-size-1 { - tab-size: 1 !important; - -moz-tab-size: 1 !important; -} - -.tab-size-2 { - tab-size: 2 !important; - -moz-tab-size: 2 !important; -} - -.tab-size-3 { - tab-size: 3 !important; - -moz-tab-size: 3 !important; -} - -.tab-size-4 { - tab-size: 4 !important; - -moz-tab-size: 4 !important; -} - -.tab-size-5 { - tab-size: 5 !important; - -moz-tab-size: 5 !important; -} - -.tab-size-6 { - tab-size: 6 !important; - -moz-tab-size: 6 !important; -} - -.tab-size-7 { - tab-size: 7 !important; - -moz-tab-size: 7 !important; -} - -.tab-size-8 { - tab-size: 8 !important; - -moz-tab-size: 8 !important; -} - -.tab-size-9 { - tab-size: 9 !important; - -moz-tab-size: 9 !important; -} - -.tab-size-10 { - tab-size: 10 !important; - -moz-tab-size: 10 !important; -} - -.tab-size-11 { - tab-size: 11 !important; - -moz-tab-size: 11 !important; -} - -.tab-size-12 { - tab-size: 12 !important; - -moz-tab-size: 12 !important; -} - -.tab-size-13 { - tab-size: 13 !important; - -moz-tab-size: 13 !important; -} - -.tab-size-14 { - tab-size: 14 !important; - -moz-tab-size: 14 !important; -} - -.tab-size-15 { - tab-size: 15 !important; - -moz-tab-size: 15 !important; -} - -.tab-size-16 { - tab-size: 16 !important; - -moz-tab-size: 16 !important; -} - .stats-table { display: table; width: 100%; @@ -2415,8 +2314,21 @@ height: 0.5em; } +.labels-list { + display: inline-flex; + flex-wrap: wrap; + gap: 2.5px; +} + +.labels-list a { + display: flex; + text-decoration: none; +} + .labels-list .label { - margin: 2px 0; + padding: 0 6px; + margin: 0 !important; + min-height: 20px; display: inline-flex !important; line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ } @@ -2428,6 +2340,11 @@ gap: 0 !important; } +.archived-label { + filter: grayscale(0.5); + opacity: 0.5; +} + .ui.label.scope-left { border-bottom-right-radius: 0; border-top-right-radius: 0; @@ -2470,13 +2387,14 @@ tbody.commit-list { .author-wrapper { overflow: hidden; text-overflow: ellipsis; - max-width: calc(100% - 50px); + max-width: 100%; display: inline-block; vertical-align: middle; } .author-wrapper { max-width: 180px; + align-self: center; } /* in the commit list, messages can wrap so we can use inline */ @@ -2494,10 +2412,6 @@ tbody.commit-list { tr.commit-list { width: 100%; } - th .message-wrapper { - display: block; - max-width: calc(100vw - 70px); - } .author-wrapper { max-width: 80px; } @@ -2507,27 +2421,18 @@ tbody.commit-list { tr.commit-list { width: 723px; } - th .message-wrapper { - max-width: 120px; - } } @media (min-width: 992px) and (max-width: 1200px) { tr.commit-list { width: 933px; } - th .message-wrapper { - max-width: 350px; - } } @media (min-width: 1201px) { tr.commit-list { width: 1127px; } - th .message-wrapper { - max-width: 525px; - } } .commit-list .commit-status-link { @@ -2573,6 +2478,7 @@ tbody.commit-list { #repo-topics .repo-topic { font-weight: var(--font-weight-normal); cursor: pointer; + margin: 0; } #new-dependency-drop-list.ui.selection.dropdown { @@ -2798,16 +2704,6 @@ tbody.commit-list { border-left: 1px solid var(--color-secondary); } -.repository .ui.menu.new-menu { - background: none !important; -} - -@media (max-width: 1200px) { - .repository .ui.menu.new-menu::after { - background: none !important; - } -} - .migrate-entries { display: grid !important; grid-template-columns: repeat(3, 1fr); @@ -2864,7 +2760,7 @@ tbody.commit-list { .repository.file.list #repo-files-table .entry td.message, .repository.file.list #repo-files-table .commit-list td.message, .repository.file.list #repo-files-table .entry span.commit-summary, - .repository.file.list #repo-files-table .commit-list span.commit-summary { + .repository.file.list #repo-files-table .commit-list tr span.commit-summary { display: none !important; } .repository.view.issue .comment-list .timeline, @@ -3000,6 +2896,7 @@ tbody.commit-list { display: flex; align-items: center; justify-content: flex-end; + gap: 8px; } @media (max-width: 767.98px) { @@ -3034,3 +2931,33 @@ tbody.commit-list { #cherry-pick-modal .scrolling.menu { max-height: 200px; } + +/* Branch tag selector - TODO: Merge this into the same selector on repo page */ +.repository .issue-content .issue-content-right .ui.grid .column.row { + padding: 10px; + padding-bottom: 0; +} +.repository .issue-content .issue-content-right .ui.grid .column.muted { + padding: 0; +} +.repository .issue-content .issue-content-right .ui.grid .column.muted .text { + display: inline-block; + padding: 10px; + width: 100%; + text-align: center; + border: 1px solid transparent; + border-bottom: none; +} +.repository .issue-content .issue-content-right .ui.grid .column.muted .text.black { + border-color: var(--color-secondary); + background: var(--color-menu); + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); +} +.repository .issue-content .issue-content-right .ui.dropdown .scrolling.menu { + border-top: none; +} +.repository .issue-content .issue-content-right .branch-tag-divider { + margin-top: -1px; + border-top: 1px solid var(--color-secondary); +} diff --git a/web_src/css/repo/header.css b/web_src/css/repo/header.css index d5c7d212e8..55e704ed10 100644 --- a/web_src/css/repo/header.css +++ b/web_src/css/repo/header.css @@ -1,4 +1,8 @@ -.fork-flag { +.repository .secondary-nav { + padding-top: 12px; +} + +.repository .secondary-nav .fork-flag { margin-top: 0.5rem; font-size: 12px; } @@ -8,17 +12,15 @@ flex-flow: row wrap; justify-content: space-between; gap: 0.5rem; + margin-bottom: 4px; } .repo-header .flex-item { padding: 0; } -.repo-header .btn.interact-bg:hover { - text-decoration: none; -} - .repo-header .flex-item-main { + flex: 0; flex-basis: unset; } @@ -26,10 +28,6 @@ flex-wrap: nowrap; } -.repo-header .flex-item-trailing .repo-icon { - display: none; -} - .repo-buttons { align-items: center; display: flex; @@ -69,33 +67,3 @@ .repo-buttons .ui.labeled.button.disabled > .button { pointer-events: none !important; } - -.repository .header-wrapper { - background-color: var(--color-header-wrapper); -} - -.repository .header-wrapper .new-menu { - padding-top: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.repository .header-wrapper .new-menu .item { - margin-left: 0 !important; - margin-right: 0 !important; -} - -@media (max-width: 767.98px) { - .repo-header .flex-item { - flex-grow: 1; - } - .repo-buttons .ui.labeled.button .text { - display: none; - } - .repo-header .flex-item-trailing .label { - display: none; - } - .repo-header .flex-item-trailing .repo-icon { - display: initial; - } -} diff --git a/web_src/css/repo/issue-card.css b/web_src/css/repo/issue-card.css index 5a70de47c2..b9368df4f6 100644 --- a/web_src/css/repo/issue-card.css +++ b/web_src/css/repo/issue-card.css @@ -19,3 +19,7 @@ font-size: 14px; margin-left: 4px; } + +.issue-card.sortable-chosen .issue-card-title { + cursor: inherit; +} diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 1421577af2..77905956f0 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -9,6 +9,7 @@ .issue-list-toolbar-left { display: flex; + align-items: center; } .issue-list-toolbar-right .filter.menu { @@ -33,23 +34,6 @@ } } -#issue-list .flex-item-title .labels-list { - display: flex; - flex-wrap: wrap; - gap: 0.25em; -} - -#issue-list .flex-item-title .labels-list a { - display: flex; - text-decoration: none; -} - -#issue-list .flex-item-title .labels-list .label { - padding: 0 6px; - margin: 0; - min-height: 20px; -} - #issue-list .flex-item-body .branches { display: inline-flex; } diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css index 1e5e51eac5..e99d0399d1 100644 --- a/web_src/css/repo/linebutton.css +++ b/web_src/css/repo/linebutton.css @@ -2,24 +2,17 @@ color: var(--color-text-dark) !important; } -.code-line-menu { - width: auto !important; - border: none !important; /* the border is provided by tippy, not using the `.ui.menu` border */ -} - .code-line-button { - background-color: var(--color-menu); - color: var(--color-text-light); border: 1px solid var(--color-secondary); border-radius: var(--border-radius); - padding: 1px 10px; + padding: 1px 4px !important; position: absolute; font-family: var(--fonts-regular); left: 0; - transform: translateX(-50%); + transform: translateX(calc(-50% + 6px)); cursor: pointer; } .code-line-button:hover { - color: var(--color-primary); + background: var(--color-secondary) !important; } diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css index 1302e9cb5c..ba502d3216 100644 --- a/web_src/css/repo/wiki.css +++ b/web_src/css/repo/wiki.css @@ -21,6 +21,7 @@ .repository.wiki .wiki-content-parts .markup { border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); padding: 1em; margin-top: 1em; font-size: 1em; @@ -58,7 +59,7 @@ } @media (max-width: 767.98px) { - .repository.wiki #clone-panel #repo-clone-url { + .repository.wiki .clone-panel #repo-clone-url { width: 160px; } .repository.wiki .wiki-content-main.with-sidebar, diff --git a/web_src/css/review.css b/web_src/css/review.css index 5336775547..7534500e6f 100644 --- a/web_src/css/review.css +++ b/web_src/css/review.css @@ -96,9 +96,6 @@ } @media (max-width: 767.98px) { - .comment-code-cloud .comments .comment { - display: flex; - } .comment-code-cloud .comments .comment .comment-header-right.actions .ui.basic.label { display: none; } @@ -194,6 +191,10 @@ margin: 0 0 0 3em; } +.diff-file-body.binary { + padding: 5px 10px; +} + .file-comment { color: var(--color-text); } diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css index e1ad6e7f97..6217b45300 100644 --- a/web_src/css/shared/flex-list.css +++ b/web_src/css/shared/flex-list.css @@ -86,7 +86,7 @@ border-top: 1px solid var(--color-secondary); } -/* Fomantic UI segment has default "padding: 1em", so here it removes the padding-top and padding-bottom accordingly (there might also be some `gt-hidden` siblings). +/* Fomantic UI segment has default "padding: 1em", so here it removes the padding-top and padding-bottom accordingly (there might also be some `tw-hidden` siblings). Developers could also use "flex-space-fitted" class to remove the first item's padding-top and the last item's padding-bottom */ .flex-list.flex-space-fitted > .flex-item:first-child, .ui.segment > .flex-list > .flex-item:first-child { diff --git a/web_src/css/shared/repoorg.css b/web_src/css/shared/repoorg.css index 7f0a805d0f..5573ae47b8 100644 --- a/web_src/css/shared/repoorg.css +++ b/web_src/css/shared/repoorg.css @@ -5,13 +5,6 @@ margin-left: 15px; } -.repository .ui.secondary.stackable.pointing.menu, -.organization .ui.secondary.stackable.pointing.menu { - flex-wrap: wrap; - margin-top: 5px; - margin-bottom: 10px; -} - .repository .ui.tabs.container, .organization .ui.tabs.container { margin-top: 14px; diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index bac002e3db..7bf2c982c6 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -3,72 +3,72 @@ :root { --is-dark-theme: true; - --color-primary: #87ab63; + --color-primary: #4183c4; --color-primary-contrast: #ffffff; - --color-primary-dark-1: #93b373; - --color-primary-dark-2: #9fbc82; - --color-primary-dark-3: #abc492; - --color-primary-dark-4: #b7cda1; - --color-primary-dark-5: #cfddc1; - --color-primary-dark-6: #e7eee0; - --color-primary-dark-7: #f8faf6; - --color-primary-light-1: #7a9e55; - --color-primary-light-2: #6c8c4c; - --color-primary-light-3: #5f7b42; - --color-primary-light-4: #516939; - --color-primary-light-5: #364626; - --color-primary-light-6: #1b2313; - --color-primary-light-7: #080b06; - --color-primary-alpha-10: #87ab6319; - --color-primary-alpha-20: #87ab6333; - --color-primary-alpha-30: #87ab634b; - --color-primary-alpha-40: #87ab6366; - --color-primary-alpha-50: #87ab6380; - --color-primary-alpha-60: #87ab6399; - --color-primary-alpha-70: #87ab63b3; - --color-primary-alpha-80: #87ab63cc; - --color-primary-alpha-90: #87ab63e1; + --color-primary-dark-1: #548fca; + --color-primary-dark-2: #679cd0; + --color-primary-dark-3: #7aa8d6; + --color-primary-dark-4: #8db5dc; + --color-primary-dark-5: #b3cde7; + --color-primary-dark-6: #d9e6f3; + --color-primary-dark-7: #f4f8fb; + --color-primary-light-1: #3876b3; + --color-primary-light-2: #31699f; + --color-primary-light-3: #2b5c8b; + --color-primary-light-4: #254f77; + --color-primary-light-5: #193450; + --color-primary-light-6: #0c1a28; + --color-primary-light-7: #04080c; + --color-primary-alpha-10: #4183c419; + --color-primary-alpha-20: #4183c433; + --color-primary-alpha-30: #4183c44b; + --color-primary-alpha-40: #4183c466; + --color-primary-alpha-50: #4183c480; + --color-primary-alpha-60: #4183c499; + --color-primary-alpha-70: #4183c4b3; + --color-primary-alpha-80: #4183c4cc; + --color-primary-alpha-90: #4183c4e1; --color-primary-hover: var(--color-primary-light-1); --color-primary-active: var(--color-primary-light-2); - --color-secondary: #525767; - --color-secondary-dark-1: #5c6374; - --color-secondary-dark-2: #666e81; - --color-secondary-dark-3: #7c8497; - --color-secondary-dark-4: #8990a1; - --color-secondary-dark-5: #959cab; - --color-secondary-dark-6: #a2a8b5; - --color-secondary-dark-7: #afb4c0; - --color-secondary-dark-8: #bcc0ca; - --color-secondary-dark-9: #c9cbd4; - --color-secondary-dark-10: #d6d7de; - --color-secondary-dark-11: #e2e3e8; - --color-secondary-dark-12: #eeeff2; - --color-secondary-dark-13: #fbfbfc; - --color-secondary-light-1: #454a57; - --color-secondary-light-2: #383c47; - --color-secondary-light-3: #2c2f37; - --color-secondary-light-4: #1f2226; - --color-secondary-alpha-10: #52576719; - --color-secondary-alpha-20: #52576733; - --color-secondary-alpha-30: #5257674b; - --color-secondary-alpha-40: #52576766; - --color-secondary-alpha-50: #52576780; - --color-secondary-alpha-60: #52576799; - --color-secondary-alpha-70: #525767b3; - --color-secondary-alpha-80: #525767cc; - --color-secondary-alpha-90: #525767e1; + --color-secondary: #3b444c; + --color-secondary-dark-1: #414b54; + --color-secondary-dark-2: #49545f; + --color-secondary-dark-3: #576471; + --color-secondary-dark-4: #677685; + --color-secondary-dark-5: #758594; + --color-secondary-dark-6: #8392a0; + --color-secondary-dark-7: #929eab; + --color-secondary-dark-8: #a2acb7; + --color-secondary-dark-9: #a9b3bd; + --color-secondary-dark-10: #b7bfc7; + --color-secondary-dark-11: #c5cbd2; + --color-secondary-dark-12: #cfd4da; + --color-secondary-dark-13: #d2d7dc; + --color-secondary-light-1: #313940; + --color-secondary-light-2: #292f35; + --color-secondary-light-3: #1d2226; + --color-secondary-light-4: #171b1e; + --color-secondary-alpha-10: #3b444c19; + --color-secondary-alpha-20: #3b444c33; + --color-secondary-alpha-30: #3b444c4b; + --color-secondary-alpha-40: #3b444c66; + --color-secondary-alpha-50: #3b444c80; + --color-secondary-alpha-60: #3b444c99; + --color-secondary-alpha-70: #3b444cb3; + --color-secondary-alpha-80: #3b444ccc; + --color-secondary-alpha-90: #3b444ce1; --color-secondary-button: var(--color-secondary-dark-4); --color-secondary-hover: var(--color-secondary-dark-3); --color-secondary-active: var(--color-secondary-dark-2); /* console colors - used for actions console and console files */ - --color-console-fg: #eeeff2; - --color-console-fg-subtle: #959cab; - --color-console-bg: #262936; - --color-console-border: #383c47; - --color-console-hover-bg: #ffffff16; - --color-console-active-bg: #454a57; - --color-console-menu-bg: #383c47; - --color-console-menu-border: #5c6374; + --color-console-fg: #f7f8f9; + --color-console-fg-subtle: #bdc4cc; + --color-console-bg: #171b1e; + --color-console-border: #2e353b; + --color-console-hover-bg: #272d33; + --color-console-active-bg: #2e353b; + --color-console-menu-bg: #262b31; + --color-console-menu-border: #414b55; /* named colors */ --color-red: #cc4848; --color-orange: #cc580c; @@ -81,7 +81,7 @@ --color-purple: #b259d0; --color-pink: #d22e8b; --color-brown: #a47252; - --color-black: #2e323e; + --color-black: #1d2328; /* light variants - produced via Sass scale-color(color, $lightness: +10%) */ --color-red-light: #d15a5a; --color-orange-light: #f6a066; @@ -94,7 +94,7 @@ --color-purple-light: #ba6ad5; --color-pink-light: #d74397; --color-brown-light: #b08061; - --color-black-light: #3f4555; + --color-black-light: #424851; /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */ --color-red-dark-1: #c23636; --color-orange-dark-1: #f38236; @@ -107,7 +107,7 @@ --color-purple-dark-1: #a742c9; --color-pink-dark-1: #be297d; --color-brown-dark-1: #94674a; - --color-black-dark-1: #292d38; + --color-black-dark-1: #292e38; /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */ --color-red-dark-2: #ad3030; --color-orange-dark-2: #f16e17; @@ -120,27 +120,27 @@ --color-purple-dark-2: #9834b9; --color-pink-dark-2: #a9246f; --color-brown-dark-2: #835b42; - --color-black-dark-2: #252832; + --color-black-dark-2: #272930; /* ansi colors used for actions console and console files */ - --color-ansi-black: var(--color-black); - --color-ansi-red: var(--color-red); - --color-ansi-green: var(--color-green); - --color-ansi-yellow: var(--color-yellow); - --color-ansi-blue: var(--color-blue); - --color-ansi-magenta: var(--color-pink); - --color-ansi-cyan: var(--color-teal); + --color-ansi-black: #1e2327; + --color-ansi-red: #cc4848; + --color-ansi-green: #87ab63; + --color-ansi-yellow: #cc9903; + --color-ansi-blue: #3a8ac6; + --color-ansi-magenta: #d22e8b; + --color-ansi-cyan: #00918a; --color-ansi-white: var(--color-console-fg-subtle); - --color-ansi-bright-black: var(--color-black-light); - --color-ansi-bright-red: var(--color-red-light); - --color-ansi-bright-green: var(--color-green-light); - --color-ansi-bright-yellow: var(--color-yellow-light); - --color-ansi-bright-blue: var(--color-blue-light); - --color-ansi-bright-magenta: var(--color-pink-light); - --color-ansi-bright-cyan: var(--color-teal-light); + --color-ansi-bright-black: #424851; + --color-ansi-bright-red: #d15a5a; + --color-ansi-bright-green: #93b373; + --color-ansi-bright-yellow: #eaaf03; + --color-ansi-bright-blue: #4e96cc; + --color-ansi-bright-magenta: #d74397; + --color-ansi-bright-cyan: #00b6ad; --color-ansi-bright-white: var(--color-console-fg); /* other colors */ - --color-grey: #505665; - --color-grey-light: #a1a6b7; + --color-grey: #384149; + --color-grey-light: #818f9e; --color-gold: #b1983b; --color-white: #ffffff; --color-diff-removed-word-bg: #6f3333; @@ -151,7 +151,7 @@ --color-diff-removed-row-border: #634343; --color-diff-moved-row-border: #bcca6f; --color-diff-added-row-border: #314a37; - --color-diff-inactive: #353846; + --color-diff-inactive: #22282d; --color-error-border: #a04141; --color-error-bg: #522; --color-error-bg-active: #744; @@ -180,60 +180,59 @@ --color-orange-badge-hover-bg: #f2711c4d; --color-git: #f05133; /* target-based colors */ - --color-body: #2e323e; - --color-box-header: #303340; - --color-box-body: #222733; - --color-box-body-highlight: #262b36; - --color-text-dark: #dbe0ea; - --color-text: #cbd0da; - --color-text-light: #bbbfca; - --color-text-light-1: #aaafb9; - --color-text-light-2: #9a9ea9; - --color-text-light-3: #8a8e99; - --color-footer: #232834; - --color-timeline: #4c525e; - --color-input-text: #dfe3ec; - --color-input-background: #1e252e; - --color-input-toggle-background: #454a57; + --color-body: #1b1f23; + --color-box-header: #1a1d1f; + --color-box-body: #14171a; + --color-box-body-highlight: #1e2226; + --color-text-dark: #f7f8f9; + --color-text: #d0d5da; + --color-text-light: #bcc3cb; + --color-text-light-1: #a5afb9; + --color-text-light-2: #8f9ba8; + --color-text-light-3: #788797; + --color-footer: var(--color-nav-bg); + --color-timeline: #343c44; + --color-input-text: var(--color-text-dark); + --color-input-background: #171a1e; + --color-input-toggle-background: #2e353c; --color-input-border: var(--color-secondary); --color-input-border-hover: var(--color-secondary-dark-1); - --color-header-wrapper: #202430; - --color-light: #00000028; + --color-light: #00001728; --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled))); - --color-light-border: #ffffff28; - --color-hover: #ffffff19; - --color-active: #ffffff24; - --color-menu: #1e252e; - --color-card: #1e252e; - --color-markup-table-row: #ffffff06; - --color-markup-code-block: #ffffff16; - --color-button: #1e252e; - --color-code-bg: #222733; - --color-code-sidebar-bg: #232834; - --color-shadow: #00000058; - --color-secondary-bg: #2a2e3a; - --color-expand-button: #3c404d; - --color-placeholder-text: #8a8e99; + --color-light-border: #e8f3ff28; + --color-hover: #e8f3ff19; + --color-active: #e8f3ff24; + --color-menu: #171a1e; + --color-card: #171a1e; + --color-markup-table-row: #e8f3ff0f; + --color-markup-code-block: #e8f3ff12; + --color-markup-code-inline: #e8f3ff28; + --color-button: #171a1e; + --color-code-bg: #14171a; + --color-shadow: #00001758; + --color-secondary-bg: #2a3137; + --color-expand-button: #2f363d; + --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--color-primary-light-5); --color-project-board-bg: var(--color-secondary-light-2); - --color-project-board-dark-label: #111111; - --color-project-board-light-label: #eeeeee; --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */ - --color-reaction-bg: #ffffff12; + --color-reaction-bg: #e8f3ff12; --color-reaction-hover-bg: var(--color-primary-light-4); --color-reaction-active-bg: var(--color-primary-light-5); - --color-tooltip-text: #ffffff; - --color-tooltip-bg: #000000f0; - --color-nav-bg: #232834; - --color-nav-hover-bg: #383c47; + --color-tooltip-text: #f9fafb; + --color-tooltip-bg: #000b17f0; + --color-nav-bg: #16191d; + --color-nav-hover-bg: var(--color-secondary-light-1); --color-nav-text: var(--color-text); - --color-label-text: #dfe3ec; - --color-label-bg: #7c84974b; - --color-label-hover-bg: #7c8497a0; - --color-label-active-bg: #7c8497ff; + --color-secondary-nav-bg: #181c20; + --color-label-text: var(--color-text); + --color-label-bg: #7282924b; + --color-label-hover-bg: #728292a0; + --color-label-active-bg: #728292ff; --color-accent: var(--color-primary-light-1); --color-small-accent: var(--color-primary-light-5); - --color-active-line: #534d1b; + --color-highlight-fg: #87651e; + --color-highlight-bg: #352c1c; --color-overlay-backdrop: #080808c0; accent-color: var(--color-accent); color-scheme: dark; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index ca5d15cd25..dfccd37647 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -30,45 +30,45 @@ --color-primary-alpha-90: #4183c4e1; --color-primary-hover: var(--color-primary-dark-1); --color-primary-active: var(--color-primary-dark-2); - --color-secondary: #dedede; - --color-secondary-dark-1: #cecece; - --color-secondary-dark-2: #bfbfbf; - --color-secondary-dark-3: #a0a0a0; - --color-secondary-dark-4: #909090; - --color-secondary-dark-5: #818181; - --color-secondary-dark-6: #717171; - --color-secondary-dark-7: #626262; - --color-secondary-dark-8: #525252; - --color-secondary-dark-9: #434343; - --color-secondary-dark-10: #333333; - --color-secondary-dark-11: #242424; - --color-secondary-dark-12: #141414; - --color-secondary-dark-13: #040404; - --color-secondary-light-1: #e5e5e5; - --color-secondary-light-2: #ebebeb; - --color-secondary-light-3: #f2f2f2; - --color-secondary-light-4: #f8f8f8; - --color-secondary-alpha-10: #dedede19; - --color-secondary-alpha-20: #dedede33; - --color-secondary-alpha-30: #dedede4b; - --color-secondary-alpha-40: #dedede66; - --color-secondary-alpha-50: #dedede80; - --color-secondary-alpha-60: #dedede99; - --color-secondary-alpha-70: #dededeb3; - --color-secondary-alpha-80: #dededecc; - --color-secondary-alpha-90: #dededee1; + --color-secondary: #d0d7de; + --color-secondary-dark-1: #c7ced5; + --color-secondary-dark-2: #b9c0c7; + --color-secondary-dark-3: #99a0a7; + --color-secondary-dark-4: #899097; + --color-secondary-dark-5: #7a8188; + --color-secondary-dark-6: #6a7178; + --color-secondary-dark-7: #5b6269; + --color-secondary-dark-8: #4b5259; + --color-secondary-dark-9: #3c434a; + --color-secondary-dark-10: #2c333a; + --color-secondary-dark-11: #1d242b; + --color-secondary-dark-12: #0d141b; + --color-secondary-dark-13: #00040b; + --color-secondary-light-1: #dee5ec; + --color-secondary-light-2: #e4ebf2; + --color-secondary-light-3: #ebf2f9; + --color-secondary-light-4: #f1f8ff; + --color-secondary-alpha-10: #d0d7de19; + --color-secondary-alpha-20: #d0d7de33; + --color-secondary-alpha-30: #d0d7de4b; + --color-secondary-alpha-40: #d0d7de66; + --color-secondary-alpha-50: #d0d7de80; + --color-secondary-alpha-60: #d0d7de99; + --color-secondary-alpha-70: #d0d7deb3; + --color-secondary-alpha-80: #d0d7decc; + --color-secondary-alpha-90: #d0d7dee1; --color-secondary-button: var(--color-secondary-dark-4); --color-secondary-hover: var(--color-secondary-dark-5); --color-secondary-active: var(--color-secondary-dark-6); /* console colors - used for actions console and console files */ - --color-console-fg: #eeeff2; - --color-console-fg-subtle: #959cab; - --color-console-bg: #262936; - --color-console-border: #383c47; - --color-console-hover-bg: #ffffff16; - --color-console-active-bg: #454a57; - --color-console-menu-bg: #383c47; - --color-console-menu-border: #5c6374; + --color-console-fg: #f7f8f9; + --color-console-fg-subtle: #bdc4cc; + --color-console-bg: #171b1e; + --color-console-border: #2e353b; + --color-console-hover-bg: #272d33; + --color-console-active-bg: #2e353b; + --color-console-menu-bg: #262b31; + --color-console-menu-border: #414b55; /* named colors */ --color-red: #db2828; --color-orange: #f2711c; @@ -81,7 +81,7 @@ --color-purple: #a333c8; --color-pink: #e03997; --color-brown: #a5673f; - --color-black: #1b1c1d; + --color-black: #1d2328; /* light variants - produced via Sass scale-color(color, $lightness: +25%) */ --color-red-light: #e45e5e; --color-orange-light: #f59555; @@ -94,7 +94,7 @@ --color-purple-light: #bb64d8; --color-pink-light: #e86bb1; --color-brown-light: #c58b66; - --color-black-light: #525558; + --color-black-light: #4b5b68; /* dark 1 variants - produced via Sass scale-color(color, $lightness: -10%) */ --color-red-dark-1: #c82121; --color-orange-dark-1: #e6630d; @@ -107,7 +107,7 @@ --color-purple-dark-1: #932eb4; --color-pink-dark-1: #db228a; --color-brown-dark-1: #955d39; - --color-black-dark-1: #18191a; + --color-black-dark-1: #2c3339; /* dark 2 variants - produced via Sass scale-color(color, $lightness: -20%) */ --color-red-dark-2: #b11e1e; --color-orange-dark-2: #cc580c; @@ -120,27 +120,27 @@ --color-purple-dark-2: #8229a0; --color-pink-dark-2: #c21e7b; --color-brown-dark-2: #845232; - --color-black-dark-2: #161617; + --color-black-dark-2: #131619; /* ansi colors used for actions console and console files */ - --color-ansi-black: var(--color-black); - --color-ansi-red: var(--color-red); - --color-ansi-green: var(--color-green); - --color-ansi-yellow: var(--color-yellow); - --color-ansi-blue: var(--color-blue); - --color-ansi-magenta: var(--color-pink); - --color-ansi-cyan: var(--color-teal); + --color-ansi-black: #1e2327; + --color-ansi-red: #cc4848; + --color-ansi-green: #87ab63; + --color-ansi-yellow: #cc9903; + --color-ansi-blue: #3a8ac6; + --color-ansi-magenta: #d22e8b; + --color-ansi-cyan: #00918a; --color-ansi-white: var(--color-console-fg-subtle); - --color-ansi-bright-black: var(--color-black-light); - --color-ansi-bright-red: var(--color-red-light); - --color-ansi-bright-green: var(--color-green-light); - --color-ansi-bright-yellow: var(--color-yellow-light); - --color-ansi-bright-blue: var(--color-blue-light); - --color-ansi-bright-magenta: var(--color-pink-light); - --color-ansi-bright-cyan: var(--color-teal-light); + --color-ansi-bright-black: #46494d; + --color-ansi-bright-red: #d15a5a; + --color-ansi-bright-green: #93b373; + --color-ansi-bright-yellow: #eaaf03; + --color-ansi-bright-blue: #4e96cc; + --color-ansi-bright-magenta: #d74397; + --color-ansi-bright-cyan: #00b6ad; --color-ansi-bright-white: var(--color-console-fg); /* other colors */ - --color-grey: #707070; - --color-grey-light: #838383; + --color-grey: #697077; + --color-grey-light: #7c838a; --color-gold: #a1882b; --color-white: #ffffff; --color-diff-removed-word-bg: #fdb8c0; @@ -151,7 +151,7 @@ --color-diff-removed-row-border: #f1c0c0; --color-diff-moved-row-border: #d0e27f; --color-diff-added-row-border: #e6ffed; - --color-diff-inactive: #f2f2f2; + --color-diff-inactive: #f0f2f4; --color-error-border: #e0b4b4; --color-error-bg: #fff6f6; --color-error-bg-active: #fbb; @@ -181,59 +181,58 @@ --color-git: #f05133; /* target-based colors */ --color-body: #ffffff; - --color-box-header: #f7f7f7; + --color-box-header: #f1f3f5; --color-box-body: #ffffff; - --color-box-body-highlight: #fafafa; - --color-text-dark: #080808; - --color-text: #212121; - --color-text-light: #555555; - --color-text-light-1: #6a6a6a; - --color-text-light-2: #808080; - --color-text-light-3: #a0a0a0; - --color-footer: #ffffff; - --color-timeline: #ececec; - --color-input-text: #212121; - --color-input-background: #fafafa; - --color-input-toggle-background: #dedede; + --color-box-body-highlight: #ecf5fd; + --color-text-dark: #01050a; + --color-text: #181c21; + --color-text-light: #30363b; + --color-text-light-1: #40474d; + --color-text-light-2: #5b6167; + --color-text-light-3: #747c84; + --color-footer: var(--color-nav-bg); + --color-timeline: #d0d7de; + --color-input-text: var(--color-text-dark); + --color-input-background: #fff; + --color-input-toggle-background: #d0d7de; --color-input-border: var(--color-secondary); --color-input-border-hover: var(--color-secondary-dark-1); - --color-header-wrapper: transparent; - --color-light: #00000006; + --color-light: #00001706; --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); - --color-light-border: #0000001d; - --color-hover: #00000014; - --color-active: #0000001b; - --color-menu: #fafafa; - --color-card: #fafafa; - --color-markup-table-row: #00000008; - --color-markup-code-block: #00000010; - --color-button: #fafafa; - --color-code-bg: #ffffff; - --color-code-sidebar-bg: #f5f5f5; - --color-shadow: #00000026; - --color-secondary-bg: #f4f4f4; - --color-expand-button: #d8efff; - --color-placeholder-text: #aaa; + --color-light-border: #0000171d; + --color-hover: #00001708; + --color-active: #00001714; + --color-menu: #f8f9fb; + --color-card: #f8f9fb; + --color-markup-table-row: #0030600a; + --color-markup-code-block: #00306010; + --color-markup-code-inline: #00306012; + --color-button: #f8f9fb; + --color-code-bg: #fafdff; + --color-shadow: #00001726; + --color-secondary-bg: #f2f5f8; + --color-expand-button: #cfe8fa; + --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--color-primary-light-6); --color-project-board-bg: var(--color-secondary-light-4); - --color-project-board-dark-label: #111111; - --color-project-board-light-label: #eeeeee; --color-caret: var(--color-text-dark); - --color-reaction-bg: #0000000a; + --color-reaction-bg: #0000170a; --color-reaction-hover-bg: var(--color-primary-light-5); --color-reaction-active-bg: var(--color-primary-light-6); - --color-tooltip-text: #ffffff; - --color-tooltip-bg: #000000f0; - --color-nav-bg: #ffffff; - --color-nav-hover-bg: #ebebeb; + --color-tooltip-text: #fbfdff; + --color-tooltip-bg: #000017f0; + --color-nav-bg: #f6f7fa; + --color-nav-hover-bg: var(--color-secondary-light-1); --color-nav-text: var(--color-text); - --color-label-text: #232323; - --color-label-bg: #cacaca5b; - --color-label-hover-bg: #cacacaa0; - --color-label-active-bg: #cacacaff; + --color-secondary-nav-bg: #f9fafb; + --color-label-text: var(--color-text); + --color-label-bg: #949da64b; + --color-label-hover-bg: #949da6a0; + --color-label-active-bg: #949da6ff; --color-accent: var(--color-primary-light-1); --color-small-accent: var(--color-primary-light-6); - --color-active-line: #fffbdd; + --color-highlight-fg: #eed200; + --color-highlight-bg: #fffbdd; --color-overlay-backdrop: #080808c0; accent-color: var(--color-accent); color-scheme: light; diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css index ad3f13bb58..49c00c4dad 100644 --- a/web_src/fomantic/build/semantic.css +++ b/web_src/fomantic/build/semantic.css @@ -156,7 +156,6 @@ width: 1.28571429em; height: 1.28571429em; border-radius: 500rem; - -webkit-animation: loader 0.6s infinite linear; animation: loader 0.6s infinite linear; border: 0.2em solid currentColor; color: #FFFFFF; @@ -530,7 +529,6 @@ border-top-left-radius: inherit; border-bottom-left-radius: inherit; text-align: center; - -webkit-animation: none; animation: none; padding: 0.78571429em 0 0.78571429em 0; margin: 0; @@ -594,7 +592,6 @@ /* Loading Icon in Labeled Button */ .ui.labeled.icon.button > .loading.icon:before { - -webkit-animation: loader 2s linear infinite; animation: loader 2s linear infinite; } @@ -2326,873 +2323,6 @@ Theme Overrides *******************************/ -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Checkbox - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Checkbox -*******************************/ - -/*-------------- - Content ----------------*/ - -.ui.checkbox { - position: relative; - display: inline-block; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - outline: none; - vertical-align: baseline; - font-style: normal; - min-height: 17px; - font-size: 1em; - line-height: 17px; - min-width: 17px; -} - -/* HTML Checkbox */ - -.ui.checkbox input[type="checkbox"], -.ui.checkbox input[type="radio"] { - cursor: pointer; - position: absolute; - top: 0; - left: 0; - opacity: 0 !important; - outline: none; - z-index: 3; - width: 17px; - height: 17px; -} - -.ui.checkbox label { - cursor: auto; - position: relative; - display: block; - padding-left: 1.85714em; - outline: none; - font-size: 1em; -} - -.ui.checkbox label:before { - position: absolute; - top: 0; - left: 0; - width: 17px; - height: 17px; - content: ''; - background: #FFFFFF; - border-radius: 0.21428571rem; - transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease; - border: 1px solid #D4D4D5; -} - -/*-------------- - Checkmark ----------------*/ - -.ui.checkbox label:after { - position: absolute; - font-size: 14px; - top: 0; - left: 0; - width: 17px; - height: 17px; - text-align: center; - opacity: 0; - color: rgba(0, 0, 0, 0.87); - transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease; -} - -/*-------------- - Label ----------------*/ - -/* Inside */ - -.ui.checkbox label, -.ui.checkbox + label { - color: rgba(0, 0, 0, 0.87); - transition: color 0.1s ease; -} - -/* Outside */ - -.ui.checkbox + label { - vertical-align: middle; -} - -/******************************* - States -*******************************/ - -/*-------------- - Hover ----------------*/ - -.ui.checkbox label:hover::before { - background: #FFFFFF; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox label:hover, -.ui.checkbox + label:hover { - color: rgba(0, 0, 0, 0.8); -} - -/*-------------- - Down ----------------*/ - -.ui.checkbox label:active::before { - background: #F9FAFB; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox label:active::after { - color: rgba(0, 0, 0, 0.95); -} - -.ui.checkbox input:active ~ label { - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Focus ----------------*/ - -.ui.checkbox input:focus ~ label:before { - background: #FFFFFF; - border-color: #96C8DA; -} - -.ui.checkbox input:focus ~ label:after { - color: rgba(0, 0, 0, 0.95); -} - -.ui.checkbox input:focus ~ label { - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Active ----------------*/ - -.ui.checkbox input:checked ~ label:before { - background: #FFFFFF; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox input:checked ~ label:after { - opacity: 1; - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Indeterminate - ---------------*/ - -.ui.checkbox input:not([type=radio]):indeterminate ~ label:before { - background: #FFFFFF; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox input:not([type=radio]):indeterminate ~ label:after { - opacity: 1; - color: rgba(0, 0, 0, 0.95); -} - -.ui.indeterminate.toggle.checkbox input:not([type=radio]):indeterminate ~ label:before { - background: rgba(0, 0, 0, 0.15); -} - -.ui.indeterminate.toggle.checkbox input:not([type=radio]) ~ label:after { - left: 1.075rem; -} - -/*-------------- - Active Focus ----------------*/ - -.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:before, -.ui.checkbox input:checked:focus ~ label:before { - background: #FFFFFF; - border-color: #96C8DA; -} - -.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:after, -.ui.checkbox input:checked:focus ~ label:after { - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Read-Only ----------------*/ - -.ui.read-only.checkbox, -.ui.read-only.checkbox label { - cursor: default; -} - -/*-------------- - Disabled - ---------------*/ - -.ui.disabled.checkbox label, -.ui.checkbox input[disabled] ~ label { - cursor: default !important; - opacity: 0.5; - color: #000000; - pointer-events: none; -} - -/*-------------- - Hidden ----------------*/ - -/* Initialized checkbox moves input below element - to prevent manually triggering */ - -.ui.checkbox input.hidden { - z-index: -1; -} - -/* Selectable Label */ - -.ui.checkbox input.hidden + label { - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -/******************************* - Types -*******************************/ - -/*-------------- - Radio - ---------------*/ - -.ui.radio.checkbox { - min-height: 15px; -} - -.ui.radio.checkbox label { - padding-left: 1.85714em; -} - -/* Box */ - -.ui.radio.checkbox label:before { - content: ''; - transform: none; - width: 15px; - height: 15px; - border-radius: 500rem; - top: 1px; - left: 0; -} - -/* Bullet */ - -.ui.radio.checkbox label:after { - border: none; - content: '' !important; - line-height: 15px; - top: 1px; - left: 0; - width: 15px; - height: 15px; - border-radius: 500rem; - transform: scale(0.46666667); - background-color: rgba(0, 0, 0, 0.87); -} - -/* Focus */ - -.ui.radio.checkbox input:focus ~ label:before { - background-color: #FFFFFF; -} - -.ui.radio.checkbox input:focus ~ label:after { - background-color: rgba(0, 0, 0, 0.95); -} - -/* Indeterminate */ - -.ui.radio.checkbox input:indeterminate ~ label:after { - opacity: 0; -} - -/* Active */ - -.ui.radio.checkbox input:checked ~ label:before { - background-color: #FFFFFF; -} - -.ui.radio.checkbox input:checked ~ label:after { - background-color: rgba(0, 0, 0, 0.95); -} - -/* Active Focus */ - -.ui.radio.checkbox input:focus:checked ~ label:before { - background-color: #FFFFFF; -} - -.ui.radio.checkbox input:focus:checked ~ label:after { - background-color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Slider - ---------------*/ - -.ui.slider.checkbox { - min-height: 1.25rem; -} - -/* Input */ - -.ui.slider.checkbox input { - width: 3.5rem; - height: 1.25rem; -} - -/* Label */ - -.ui.slider.checkbox label { - padding-left: 4.5rem; - line-height: 1rem; - color: rgba(0, 0, 0, 0.4); -} - -/* Line */ - -.ui.slider.checkbox label:before { - display: block; - position: absolute; - content: ''; - transform: none; - border: none !important; - left: 0; - z-index: 1; - top: 0.4rem; - background-color: rgba(0, 0, 0, 0.05); - width: 3.5rem; - height: 0.21428571rem; - border-radius: 500rem; - transition: background 0.3s ease; -} - -/* Handle */ - -.ui.slider.checkbox label:after { - background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05)); - position: absolute; - content: '' !important; - opacity: 1; - z-index: 2; - border: none; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; - width: 1.5rem; - height: 1.5rem; - top: -0.25rem; - left: 0; - transform: none; - border-radius: 500rem; - transition: left 0.3s ease; -} - -/* Focus */ - -.ui.slider.checkbox input:focus ~ label:before { - background-color: rgba(0, 0, 0, 0.15); - border: none; -} - -/* Hover */ - -.ui.slider.checkbox label:hover { - color: rgba(0, 0, 0, 0.8); -} - -.ui.slider.checkbox label:hover::before { - background: rgba(0, 0, 0, 0.15); -} - -/* Active */ - -.ui.slider.checkbox input:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.slider.checkbox input:checked ~ label:before { - background-color: #545454 !important; -} - -.ui.slider.checkbox input:checked ~ label:after { - left: 2rem; -} - -/* Active Focus */ - -.ui.slider.checkbox input:focus:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.slider.checkbox input:focus:checked ~ label:before { - background-color: #000000 !important; -} - -/*-------------- - Toggle - ---------------*/ - -.ui.toggle.checkbox { - min-height: 1.5rem; -} - -/* Input */ - -.ui.toggle.checkbox input { - width: 3.5rem; - height: 1.5rem; -} - -/* Label */ - -.ui.toggle.checkbox label { - min-height: 1.5rem; - padding-left: 4.5rem; - color: rgba(0, 0, 0, 0.87); -} - -.ui.toggle.checkbox label { - padding-top: 0.15em; -} - -/* Switch */ - -.ui.toggle.checkbox label:before { - display: block; - position: absolute; - content: ''; - z-index: 1; - transform: none; - border: none; - top: 0; - background: rgba(0, 0, 0, 0.05); - box-shadow: none; - width: 3.5rem; - height: 1.5rem; - border-radius: 500rem; -} - -/* Handle */ - -.ui.toggle.checkbox label:after { - background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05)); - position: absolute; - content: '' !important; - opacity: 1; - z-index: 2; - border: none; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; - width: 1.5rem; - height: 1.5rem; - top: 0; - left: 0; - border-radius: 500rem; - transition: background 0.3s ease, left 0.3s ease; -} - -.ui.toggle.checkbox input ~ label:after { - left: -0.05rem; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; -} - -/* Focus */ - -.ui.toggle.checkbox input:focus ~ label:before { - background-color: rgba(0, 0, 0, 0.15); - border: none; -} - -/* Hover */ - -.ui.toggle.checkbox label:hover::before { - background-color: rgba(0, 0, 0, 0.15); - border: none; -} - -/* Active */ - -.ui.toggle.checkbox input:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.toggle.checkbox input:checked ~ label:before { - background-color: #2185D0 !important; -} - -.ui.toggle.checkbox input:checked ~ label:after { - left: 2.15rem; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; -} - -/* Active Focus */ - -.ui.toggle.checkbox input:focus:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.toggle.checkbox input:focus:checked ~ label:before { - background-color: #0d71bb !important; -} - -/******************************* - Variations -*******************************/ - -/*-------------- - Fitted - ---------------*/ - -.ui.fitted.checkbox label { - padding-left: 0 !important; -} - -.ui.fitted.toggle.checkbox { - width: 3.5rem; -} - -.ui.fitted.slider.checkbox { - width: 3.5rem; -} - -/*-------------------- - Size ----------------------*/ - -.ui.mini.checkbox { - font-size: 0.78571429em; -} - -.ui.tiny.checkbox { - font-size: 0.85714286em; -} - -.ui.small.checkbox { - font-size: 0.92857143em; -} - -.ui.large.checkbox { - font-size: 1.14285714em; -} - -.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.14285714); - transform-origin: left; -} - -.ui.large.form .checkbox.radio label:before, -.ui.large.checkbox.radio label:before { - transform: scale(1.14285714); - transform-origin: left; -} - -.ui.large.form .checkbox.radio label:after, -.ui.large.checkbox.radio label:after { - transform: scale(0.57142857); - transform-origin: left; - left: 0.33571429em; -} - -.ui.big.checkbox { - font-size: 1.28571429em; -} - -.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.28571429); - transform-origin: left; -} - -.ui.big.form .checkbox.radio label:before, -.ui.big.checkbox.radio label:before { - transform: scale(1.28571429); - transform-origin: left; -} - -.ui.big.form .checkbox.radio label:after, -.ui.big.checkbox.radio label:after { - transform: scale(0.64285714); - transform-origin: left; - left: 0.37142857em; -} - -.ui.huge.checkbox { - font-size: 1.42857143em; -} - -.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.42857143); - transform-origin: left; -} - -.ui.huge.form .checkbox.radio label:before, -.ui.huge.checkbox.radio label:before { - transform: scale(1.42857143); - transform-origin: left; -} - -.ui.huge.form .checkbox.radio label:after, -.ui.huge.checkbox.radio label:after { - transform: scale(0.71428571); - transform-origin: left; - left: 0.40714286em; -} - -.ui.massive.checkbox { - font-size: 1.71428571em; -} - -.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.71428571); - transform-origin: left; -} - -.ui.massive.form .checkbox.radio label:before, -.ui.massive.checkbox.radio label:before { - transform: scale(1.71428571); - transform-origin: left; -} - -.ui.massive.form .checkbox.radio label:after, -.ui.massive.checkbox.radio label:after { - transform: scale(0.85714286); - transform-origin: left; - left: 0.47857143em; -} - -/******************************* - Theme Overrides -*******************************/ - -@font-face { - font-family: 'Checkbox'; - src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype'); -} - -/* Checkmark */ - -.ui.checkbox label:after, -.ui.checkbox .box:after { - font-family: 'Checkbox'; -} - -/* Checked */ - -.ui.checkbox input:checked ~ .box:after, -.ui.checkbox input:checked ~ label:after { - content: '\e800'; -} - -/* Indeterminate */ - -.ui.checkbox input:indeterminate ~ .box:after, -.ui.checkbox input:indeterminate ~ label:after { - font-size: 12px; - content: '\e801'; -} - -/* UTF Reference -.check:before { content: '\e800'; } -.dash:before { content: '\e801'; } -.plus:before { content: '\e802'; } -*/ - -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Container - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Container -*******************************/ - -/* All Sizes */ - -.ui.container { - display: block; - max-width: 100%; -} - -/* Mobile */ - -@media only screen and (max-width: 767.98px) { - .ui.ui.ui.container:not(.fluid) { - width: auto; - margin-left: 1em; - margin-right: 1em; - } - - .ui.ui.ui.grid.container { - width: auto; - } - - .ui.ui.ui.relaxed.grid.container { - width: auto; - } - - .ui.ui.ui.very.relaxed.grid.container { - width: auto; - } -} - -/* Tablet */ - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui.ui.ui.container:not(.fluid) { - width: 723px; - margin-left: auto; - margin-right: auto; - } - - .ui.ui.ui.grid.container { - width: calc(723px + 2rem); - } - - .ui.ui.ui.relaxed.grid.container { - width: calc(723px + 3rem); - } - - .ui.ui.ui.very.relaxed.grid.container { - width: calc(723px + 5rem); - } -} - -/* Small Monitor */ - -@media only screen and (min-width: 992px) and (max-width: 1199.98px) { - .ui.ui.ui.container:not(.fluid) { - width: 933px; - margin-left: auto; - margin-right: auto; - } - - .ui.ui.ui.grid.container { - width: calc(933px + 2rem); - } - - .ui.ui.ui.relaxed.grid.container { - width: calc(933px + 3rem); - } - - .ui.ui.ui.very.relaxed.grid.container { - width: calc(933px + 5rem); - } -} - -/* Large Monitor */ - -@media only screen and (min-width: 1200px) { - .ui.ui.ui.container:not(.fluid) { - width: 1127px; - margin-left: auto; - margin-right: auto; - } - - .ui.ui.ui.grid.container { - width: calc(1127px + 2rem); - } - - .ui.ui.ui.relaxed.grid.container { - width: calc(1127px + 3rem); - } - - .ui.ui.ui.very.relaxed.grid.container { - width: calc(1127px + 5rem); - } -} - -/******************************* - Types -*******************************/ - -/* Text Container */ - -.ui.text.container { - font-family: var(--fonts-regular); - max-width: 700px; - line-height: 1.5; - font-size: 1.14285714rem; -} - -/* Fluid */ - -.ui.fluid.container { - width: 100%; -} - -/******************************* - Variations -*******************************/ - -.ui[class*="left aligned"].container { - text-align: left; -} - -.ui[class*="center aligned"].container { - text-align: center; -} - -.ui[class*="right aligned"].container { - text-align: right; -} - -.ui.justified.container { - text-align: justify; - -webkit-hyphens: auto; - hyphens: auto; -} - -/******************************* - Theme Overrides -*******************************/ - /******************************* Site Overrides *******************************/ @@ -3227,9 +2357,7 @@ background: rgba(0, 0, 0, 0.85); opacity: 0; line-height: 1; - -webkit-animation-fill-mode: both; animation-fill-mode: both; - -webkit-animation-duration: 0.5s; animation-duration: 0.5s; transition: background-color 0.5s linear; flex-direction: column; @@ -3458,44 +2586,25 @@ body.dimmable > .dimmer { } .ui[class*="center dimmer"].transition[class*="fade up"].in { - -webkit-animation-name: fadeInUpCenter; animation-name: fadeInUpCenter; } .ui[class*="center dimmer"].transition[class*="fade down"].in { - -webkit-animation-name: fadeInDownCenter; animation-name: fadeInDownCenter; } .ui[class*="center dimmer"].transition[class*="fade up"].out { - -webkit-animation-name: fadeOutUpCenter; animation-name: fadeOutUpCenter; } .ui[class*="center dimmer"].transition[class*="fade down"].out { - -webkit-animation-name: fadeOutDownCenter; animation-name: fadeOutDownCenter; } .ui[class*="center dimmer"].bounce.transition { - -webkit-animation-name: bounceCenter; animation-name: bounceCenter; } -@-webkit-keyframes fadeInUpCenter { - 0% { - opacity: 0; - transform: translateY(-40%); - -webkit-transform: translateY(calc(-40% - 0.5px)); - } - - 100% { - opacity: 1; - transform: translateY(-50%); - -webkit-transform: translateY(calc(-50% - 0.5px)); - } -} - @keyframes fadeInUpCenter { 0% { opacity: 0; @@ -3510,20 +2619,6 @@ body.dimmable > .dimmer { } } -@-webkit-keyframes fadeInDownCenter { - 0% { - opacity: 0; - transform: translateY(-60%); - -webkit-transform: translateY(calc(-60% - 0.5px)); - } - - 100% { - opacity: 1; - transform: translateY(-50%); - -webkit-transform: translateY(calc(-50% - 0.5px)); - } -} - @keyframes fadeInDownCenter { 0% { opacity: 0; @@ -3538,20 +2633,6 @@ body.dimmable > .dimmer { } } -@-webkit-keyframes fadeOutUpCenter { - 0% { - opacity: 1; - transform: translateY(-50%); - -webkit-transform: translateY(calc(-50% - 0.5px)); - } - - 100% { - opacity: 0; - transform: translateY(-45%); - -webkit-transform: translateY(calc(-45% - 0.5px)); - } -} - @keyframes fadeOutUpCenter { 0% { opacity: 1; @@ -3566,20 +2647,6 @@ body.dimmable > .dimmer { } } -@-webkit-keyframes fadeOutDownCenter { - 0% { - opacity: 1; - transform: translateY(-50%); - -webkit-transform: translateY(calc(-50% - 0.5px)); - } - - 100% { - opacity: 0; - transform: translateY(-55%); - -webkit-transform: translateY(calc(-55% - 0.5px)); - } -} - @keyframes fadeOutDownCenter { 0% { opacity: 1; @@ -3594,21 +2661,6 @@ body.dimmable > .dimmer { } } -@-webkit-keyframes bounceCenter { - 0%, 20%, 50%, 80%, 100% { - transform: translateY(-50%); - -webkit-transform: translateY(calc(-50% - 0.5px)); - } - - 40% { - transform: translateY(calc(-50% - 30px)); - } - - 60% { - transform: translateY(calc(-50% - 15px)); - } -} - @keyframes bounceCenter { 0%, 20%, 50%, 80%, 100% { transform: translateY(-50%); @@ -3672,7 +2724,6 @@ body.dimmable > .dimmer { display: none; outline: none; top: 100%; - min-width: -webkit-max-content; min-width: -moz-max-content; min-width: max-content; margin: 0; @@ -4068,7 +3119,6 @@ select.ui.dropdown { .ui.selection.dropdown .menu { overflow-x: hidden; overflow-y: auto; - -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-overflow-scrolling: touch; border-top-width: 0 !important; @@ -4280,7 +3330,6 @@ select.ui.dropdown { @supports (-webkit-touch-callout: none) or (-webkit-overflow-scrolling: touch) or (-moz-appearance:none) { @media (-moz-touch-enabled), (pointer: coarse) { .ui.dropdown .scrollhint.menu:not(.hidden):before { - -webkit-animation: scrollhint 2s ease 2; animation: scrollhint 2s ease 2; content: ''; z-index: 15; @@ -4301,18 +3350,6 @@ select.ui.dropdown { border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%; } -@-webkit-keyframes scrollhint { - 0% { - opacity: 1; - top: 100%; - } - - 100% { - opacity: 0; - top: 0; - } -} - @keyframes scrollhint { 0% { opacity: 1; @@ -4414,7 +3451,6 @@ select.ui.dropdown { .ui.search.dropdown .menu { overflow-x: hidden; overflow-y: auto; - -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-overflow-scrolling: touch; } @@ -4682,7 +3718,6 @@ select.ui.dropdown { margin: -0.64285714em 0 0 -0.64285714em; width: 1.28571429em; height: 1.28571429em; - -webkit-animation: loader 0.6s infinite linear; animation: loader 0.6s infinite linear; border: 0.2em solid #767676; border-radius: 500rem; @@ -5072,7 +4107,6 @@ select.ui.dropdown { .ui.scrolling.dropdown .menu { overflow-x: hidden; overflow-y: auto; - -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-overflow-scrolling: touch; min-width: 100% !important; @@ -5564,7 +4598,6 @@ select.ui.dropdown { line-height: 1; height: 1em; width: 1.23em; - -webkit-backface-visibility: hidden; backface-visibility: hidden; font-weight: normal; font-style: normal; @@ -7052,7 +6085,6 @@ select.ui.dropdown { margin: -1.5em 0 0 -1.5em; width: 3em; height: 3em; - -webkit-animation: loader 0.6s infinite linear; animation: loader 0.6s infinite linear; border: 0.2em solid #767676; border-radius: 500rem; @@ -7445,5580 +6477,6 @@ select.ui.dropdown { /******************************* Site Overrides *******************************/ -/*! - * # Fomantic-UI - Grid - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Standard -*******************************/ - -.ui.grid { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: stretch; - padding: 0; -} - -/*---------------------- - Remove Gutters ------------------------*/ - -.ui.grid { - margin-top: -1rem; - margin-bottom: -1rem; - margin-left: -1rem; - margin-right: -1rem; -} - -.ui.relaxed.grid { - margin-left: -1.5rem; - margin-right: -1.5rem; -} - -.ui[class*="very relaxed"].grid { - margin-left: -2.5rem; - margin-right: -2.5rem; -} - -/* Preserve Rows Spacing on Consecutive Grids */ - -.ui.grid + .grid { - margin-top: 1rem; -} - -/*------------------- - Columns ---------------------*/ - -/* Standard 16 column */ - -.ui.grid > .column:not(.row), -.ui.grid > .row > .column { - position: relative; - display: inline-block; - width: 6.25%; - padding-left: 1rem; - padding-right: 1rem; - vertical-align: top; -} - -.ui.grid > * { - padding-left: 1rem; - padding-right: 1rem; -} - -/*------------------- - Rows ---------------------*/ - -.ui.grid > .row { - position: relative; - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: inherit; - align-items: stretch; - width: 100% !important; - padding: 0; - padding-top: 1rem; - padding-bottom: 1rem; -} - -/*------------------- - Columns ---------------------*/ - -/* Vertical padding when no rows */ - -.ui.grid > .column:not(.row) { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.ui.grid > .row > .column { - margin-top: 0; - margin-bottom: 0; -} - -/*------------------- - Content ---------------------*/ - -.ui.grid > .row > img, -.ui.grid > .row > .column > img { - max-width: 100%; -} - -/*------------------- - Loose Coupling ---------------------*/ - -/* Collapse Margin on Consecutive Grid */ - -.ui.grid > .ui.grid:first-child { - margin-top: 0; -} - -.ui.grid > .ui.grid:last-child { - margin-bottom: 0; -} - -/* Segment inside Aligned Grid */ - -.ui.grid .aligned.row > .column > .segment:not(.compact):not(.attached), -.ui.aligned.grid .column > .segment:not(.compact):not(.attached) { - width: 100%; -} - -/* Align Dividers with Gutter */ - -.ui.grid .row + .ui.divider { - flex-grow: 1; - margin: 1rem 1rem; -} - -.ui.grid .column + .ui.vertical.divider { - height: calc(50% - 1rem); -} - -/* Remove Border on Last Horizontal Segment */ - -.ui.grid > .row > .column:last-child > .horizontal.segment, -.ui.grid > .column:last-child > .horizontal.segment { - box-shadow: none; -} - -/******************************* - Variations -*******************************/ - -/*----------------------- - Page Grid - -------------------------*/ - -@media only screen and (max-width: 767.98px) { - .ui.page.grid { - width: auto; - padding-left: 0; - padding-right: 0; - margin-left: 0; - margin-right: 0; - } -} - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui.page.grid { - width: auto; - margin-left: 0; - margin-right: 0; - padding-left: 2em; - padding-right: 2em; - } -} - -@media only screen and (min-width: 992px) and (max-width: 1199.98px) { - .ui.page.grid { - width: auto; - margin-left: 0; - margin-right: 0; - padding-left: 3%; - padding-right: 3%; - } -} - -@media only screen and (min-width: 1200px) and (max-width: 1919.98px) { - .ui.page.grid { - width: auto; - margin-left: 0; - margin-right: 0; - padding-left: 15%; - padding-right: 15%; - } -} - -@media only screen and (min-width: 1920px) { - .ui.page.grid { - width: auto; - margin-left: 0; - margin-right: 0; - padding-left: 23%; - padding-right: 23%; - } -} - -/*------------------- - Column Count ---------------------*/ - -/* Assume full width with one column */ - -.ui.grid > .column:only-child, -.ui.grid > .row > .column:only-child { - width: 100%; -} - -/* Grid Based */ - -.ui[class*="one column"].grid > .row > .column, -.ui[class*="one column"].grid > .column:not(.row) { - width: 100%; -} - -.ui[class*="two column"].grid > .row > .column, -.ui[class*="two column"].grid > .column:not(.row) { - width: 50%; -} - -.ui[class*="three column"].grid > .row > .column, -.ui[class*="three column"].grid > .column:not(.row) { - width: 33.33333333%; -} - -.ui[class*="four column"].grid > .row > .column, -.ui[class*="four column"].grid > .column:not(.row) { - width: 25%; -} - -.ui[class*="five column"].grid > .row > .column, -.ui[class*="five column"].grid > .column:not(.row) { - width: 20%; -} - -.ui[class*="six column"].grid > .row > .column, -.ui[class*="six column"].grid > .column:not(.row) { - width: 16.66666667%; -} - -.ui[class*="seven column"].grid > .row > .column, -.ui[class*="seven column"].grid > .column:not(.row) { - width: 14.28571429%; -} - -.ui[class*="eight column"].grid > .row > .column, -.ui[class*="eight column"].grid > .column:not(.row) { - width: 12.5%; -} - -.ui[class*="nine column"].grid > .row > .column, -.ui[class*="nine column"].grid > .column:not(.row) { - width: 11.11111111%; -} - -.ui[class*="ten column"].grid > .row > .column, -.ui[class*="ten column"].grid > .column:not(.row) { - width: 10%; -} - -.ui[class*="eleven column"].grid > .row > .column, -.ui[class*="eleven column"].grid > .column:not(.row) { - width: 9.09090909%; -} - -.ui[class*="twelve column"].grid > .row > .column, -.ui[class*="twelve column"].grid > .column:not(.row) { - width: 8.33333333%; -} - -.ui[class*="thirteen column"].grid > .row > .column, -.ui[class*="thirteen column"].grid > .column:not(.row) { - width: 7.69230769%; -} - -.ui[class*="fourteen column"].grid > .row > .column, -.ui[class*="fourteen column"].grid > .column:not(.row) { - width: 7.14285714%; -} - -.ui[class*="fifteen column"].grid > .row > .column, -.ui[class*="fifteen column"].grid > .column:not(.row) { - width: 6.66666667%; -} - -.ui[class*="sixteen column"].grid > .row > .column, -.ui[class*="sixteen column"].grid > .column:not(.row) { - width: 6.25%; -} - -/* Row Based Overrides */ - -.ui.grid > [class*="one column"].row > .column { - width: 100% !important; -} - -.ui.grid > [class*="two column"].row > .column { - width: 50% !important; -} - -.ui.grid > [class*="three column"].row > .column { - width: 33.33333333% !important; -} - -.ui.grid > [class*="four column"].row > .column { - width: 25% !important; -} - -.ui.grid > [class*="five column"].row > .column { - width: 20% !important; -} - -.ui.grid > [class*="six column"].row > .column { - width: 16.66666667% !important; -} - -.ui.grid > [class*="seven column"].row > .column { - width: 14.28571429% !important; -} - -.ui.grid > [class*="eight column"].row > .column { - width: 12.5% !important; -} - -.ui.grid > [class*="nine column"].row > .column { - width: 11.11111111% !important; -} - -.ui.grid > [class*="ten column"].row > .column { - width: 10% !important; -} - -.ui.grid > [class*="eleven column"].row > .column { - width: 9.09090909% !important; -} - -.ui.grid > [class*="twelve column"].row > .column { - width: 8.33333333% !important; -} - -.ui.grid > [class*="thirteen column"].row > .column { - width: 7.69230769% !important; -} - -.ui.grid > [class*="fourteen column"].row > .column { - width: 7.14285714% !important; -} - -.ui.grid > [class*="fifteen column"].row > .column { - width: 6.66666667% !important; -} - -.ui.grid > [class*="sixteen column"].row > .column { - width: 6.25% !important; -} - -/* Celled Page */ - -.ui.celled.page.grid { - box-shadow: none; -} - -/*------------------- - Column Width ---------------------*/ - -/* Sizing Combinations */ - -.ui.grid > .row > [class*="one wide"].column, -.ui.grid > .column.row > [class*="one wide"].column, -.ui.grid > [class*="one wide"].column, -.ui.column.grid > [class*="one wide"].column { - width: 6.25% !important; -} - -.ui.grid > .row > [class*="two wide"].column, -.ui.grid > .column.row > [class*="two wide"].column, -.ui.grid > [class*="two wide"].column, -.ui.column.grid > [class*="two wide"].column { - width: 12.5% !important; -} - -.ui.grid > .row > [class*="three wide"].column, -.ui.grid > .column.row > [class*="three wide"].column, -.ui.grid > [class*="three wide"].column, -.ui.column.grid > [class*="three wide"].column { - width: 18.75% !important; -} - -.ui.grid > .row > [class*="four wide"].column, -.ui.grid > .column.row > [class*="four wide"].column, -.ui.grid > [class*="four wide"].column, -.ui.column.grid > [class*="four wide"].column { - width: 25% !important; -} - -.ui.grid > .row > [class*="five wide"].column, -.ui.grid > .column.row > [class*="five wide"].column, -.ui.grid > [class*="five wide"].column, -.ui.column.grid > [class*="five wide"].column { - width: 31.25% !important; -} - -.ui.grid > .row > [class*="six wide"].column, -.ui.grid > .column.row > [class*="six wide"].column, -.ui.grid > [class*="six wide"].column, -.ui.column.grid > [class*="six wide"].column { - width: 37.5% !important; -} - -.ui.grid > .row > [class*="seven wide"].column, -.ui.grid > .column.row > [class*="seven wide"].column, -.ui.grid > [class*="seven wide"].column, -.ui.column.grid > [class*="seven wide"].column { - width: 43.75% !important; -} - -.ui.grid > .row > [class*="eight wide"].column, -.ui.grid > .column.row > [class*="eight wide"].column, -.ui.grid > [class*="eight wide"].column, -.ui.column.grid > [class*="eight wide"].column { - width: 50% !important; -} - -.ui.grid > .row > [class*="nine wide"].column, -.ui.grid > .column.row > [class*="nine wide"].column, -.ui.grid > [class*="nine wide"].column, -.ui.column.grid > [class*="nine wide"].column { - width: 56.25% !important; -} - -.ui.grid > .row > [class*="ten wide"].column, -.ui.grid > .column.row > [class*="ten wide"].column, -.ui.grid > [class*="ten wide"].column, -.ui.column.grid > [class*="ten wide"].column { - width: 62.5% !important; -} - -.ui.grid > .row > [class*="eleven wide"].column, -.ui.grid > .column.row > [class*="eleven wide"].column, -.ui.grid > [class*="eleven wide"].column, -.ui.column.grid > [class*="eleven wide"].column { - width: 68.75% !important; -} - -.ui.grid > .row > [class*="twelve wide"].column, -.ui.grid > .column.row > [class*="twelve wide"].column, -.ui.grid > [class*="twelve wide"].column, -.ui.column.grid > [class*="twelve wide"].column { - width: 75% !important; -} - -.ui.grid > .row > [class*="thirteen wide"].column, -.ui.grid > .column.row > [class*="thirteen wide"].column, -.ui.grid > [class*="thirteen wide"].column, -.ui.column.grid > [class*="thirteen wide"].column { - width: 81.25% !important; -} - -.ui.grid > .row > [class*="fourteen wide"].column, -.ui.grid > .column.row > [class*="fourteen wide"].column, -.ui.grid > [class*="fourteen wide"].column, -.ui.column.grid > [class*="fourteen wide"].column { - width: 87.5% !important; -} - -.ui.grid > .row > [class*="fifteen wide"].column, -.ui.grid > .column.row > [class*="fifteen wide"].column, -.ui.grid > [class*="fifteen wide"].column, -.ui.column.grid > [class*="fifteen wide"].column { - width: 93.75% !important; -} - -.ui.grid > .row > [class*="sixteen wide"].column, -.ui.grid > .column.row > [class*="sixteen wide"].column, -.ui.grid > [class*="sixteen wide"].column, -.ui.column.grid > [class*="sixteen wide"].column { - width: 100% !important; -} - -/*---------------------- - Width per Device ------------------------*/ - -/* Mobile Sizing Combinations */ - -@media only screen and (min-width: 320px) and (max-width: 767.98px) { - .ui.grid > .row > [class*="one wide mobile"].column, - .ui.grid > .column.row > [class*="one wide mobile"].column, - .ui.grid > [class*="one wide mobile"].column, - .ui.column.grid > [class*="one wide mobile"].column { - width: 6.25% !important; - } - - .ui.grid > .row > [class*="two wide mobile"].column, - .ui.grid > .column.row > [class*="two wide mobile"].column, - .ui.grid > [class*="two wide mobile"].column, - .ui.column.grid > [class*="two wide mobile"].column { - width: 12.5% !important; - } - - .ui.grid > .row > [class*="three wide mobile"].column, - .ui.grid > .column.row > [class*="three wide mobile"].column, - .ui.grid > [class*="three wide mobile"].column, - .ui.column.grid > [class*="three wide mobile"].column { - width: 18.75% !important; - } - - .ui.grid > .row > [class*="four wide mobile"].column, - .ui.grid > .column.row > [class*="four wide mobile"].column, - .ui.grid > [class*="four wide mobile"].column, - .ui.column.grid > [class*="four wide mobile"].column { - width: 25% !important; - } - - .ui.grid > .row > [class*="five wide mobile"].column, - .ui.grid > .column.row > [class*="five wide mobile"].column, - .ui.grid > [class*="five wide mobile"].column, - .ui.column.grid > [class*="five wide mobile"].column { - width: 31.25% !important; - } - - .ui.grid > .row > [class*="six wide mobile"].column, - .ui.grid > .column.row > [class*="six wide mobile"].column, - .ui.grid > [class*="six wide mobile"].column, - .ui.column.grid > [class*="six wide mobile"].column { - width: 37.5% !important; - } - - .ui.grid > .row > [class*="seven wide mobile"].column, - .ui.grid > .column.row > [class*="seven wide mobile"].column, - .ui.grid > [class*="seven wide mobile"].column, - .ui.column.grid > [class*="seven wide mobile"].column { - width: 43.75% !important; - } - - .ui.grid > .row > [class*="eight wide mobile"].column, - .ui.grid > .column.row > [class*="eight wide mobile"].column, - .ui.grid > [class*="eight wide mobile"].column, - .ui.column.grid > [class*="eight wide mobile"].column { - width: 50% !important; - } - - .ui.grid > .row > [class*="nine wide mobile"].column, - .ui.grid > .column.row > [class*="nine wide mobile"].column, - .ui.grid > [class*="nine wide mobile"].column, - .ui.column.grid > [class*="nine wide mobile"].column { - width: 56.25% !important; - } - - .ui.grid > .row > [class*="ten wide mobile"].column, - .ui.grid > .column.row > [class*="ten wide mobile"].column, - .ui.grid > [class*="ten wide mobile"].column, - .ui.column.grid > [class*="ten wide mobile"].column { - width: 62.5% !important; - } - - .ui.grid > .row > [class*="eleven wide mobile"].column, - .ui.grid > .column.row > [class*="eleven wide mobile"].column, - .ui.grid > [class*="eleven wide mobile"].column, - .ui.column.grid > [class*="eleven wide mobile"].column { - width: 68.75% !important; - } - - .ui.grid > .row > [class*="twelve wide mobile"].column, - .ui.grid > .column.row > [class*="twelve wide mobile"].column, - .ui.grid > [class*="twelve wide mobile"].column, - .ui.column.grid > [class*="twelve wide mobile"].column { - width: 75% !important; - } - - .ui.grid > .row > [class*="thirteen wide mobile"].column, - .ui.grid > .column.row > [class*="thirteen wide mobile"].column, - .ui.grid > [class*="thirteen wide mobile"].column, - .ui.column.grid > [class*="thirteen wide mobile"].column { - width: 81.25% !important; - } - - .ui.grid > .row > [class*="fourteen wide mobile"].column, - .ui.grid > .column.row > [class*="fourteen wide mobile"].column, - .ui.grid > [class*="fourteen wide mobile"].column, - .ui.column.grid > [class*="fourteen wide mobile"].column { - width: 87.5% !important; - } - - .ui.grid > .row > [class*="fifteen wide mobile"].column, - .ui.grid > .column.row > [class*="fifteen wide mobile"].column, - .ui.grid > [class*="fifteen wide mobile"].column, - .ui.column.grid > [class*="fifteen wide mobile"].column { - width: 93.75% !important; - } - - .ui.grid > .row > [class*="sixteen wide mobile"].column, - .ui.grid > .column.row > [class*="sixteen wide mobile"].column, - .ui.grid > [class*="sixteen wide mobile"].column, - .ui.column.grid > [class*="sixteen wide mobile"].column { - width: 100% !important; - } -} - -/* Tablet Sizing Combinations */ - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui.grid > .row > [class*="one wide tablet"].column, - .ui.grid > .column.row > [class*="one wide tablet"].column, - .ui.grid > [class*="one wide tablet"].column, - .ui.column.grid > [class*="one wide tablet"].column { - width: 6.25% !important; - } - - .ui.grid > .row > [class*="two wide tablet"].column, - .ui.grid > .column.row > [class*="two wide tablet"].column, - .ui.grid > [class*="two wide tablet"].column, - .ui.column.grid > [class*="two wide tablet"].column { - width: 12.5% !important; - } - - .ui.grid > .row > [class*="three wide tablet"].column, - .ui.grid > .column.row > [class*="three wide tablet"].column, - .ui.grid > [class*="three wide tablet"].column, - .ui.column.grid > [class*="three wide tablet"].column { - width: 18.75% !important; - } - - .ui.grid > .row > [class*="four wide tablet"].column, - .ui.grid > .column.row > [class*="four wide tablet"].column, - .ui.grid > [class*="four wide tablet"].column, - .ui.column.grid > [class*="four wide tablet"].column { - width: 25% !important; - } - - .ui.grid > .row > [class*="five wide tablet"].column, - .ui.grid > .column.row > [class*="five wide tablet"].column, - .ui.grid > [class*="five wide tablet"].column, - .ui.column.grid > [class*="five wide tablet"].column { - width: 31.25% !important; - } - - .ui.grid > .row > [class*="six wide tablet"].column, - .ui.grid > .column.row > [class*="six wide tablet"].column, - .ui.grid > [class*="six wide tablet"].column, - .ui.column.grid > [class*="six wide tablet"].column { - width: 37.5% !important; - } - - .ui.grid > .row > [class*="seven wide tablet"].column, - .ui.grid > .column.row > [class*="seven wide tablet"].column, - .ui.grid > [class*="seven wide tablet"].column, - .ui.column.grid > [class*="seven wide tablet"].column { - width: 43.75% !important; - } - - .ui.grid > .row > [class*="eight wide tablet"].column, - .ui.grid > .column.row > [class*="eight wide tablet"].column, - .ui.grid > [class*="eight wide tablet"].column, - .ui.column.grid > [class*="eight wide tablet"].column { - width: 50% !important; - } - - .ui.grid > .row > [class*="nine wide tablet"].column, - .ui.grid > .column.row > [class*="nine wide tablet"].column, - .ui.grid > [class*="nine wide tablet"].column, - .ui.column.grid > [class*="nine wide tablet"].column { - width: 56.25% !important; - } - - .ui.grid > .row > [class*="ten wide tablet"].column, - .ui.grid > .column.row > [class*="ten wide tablet"].column, - .ui.grid > [class*="ten wide tablet"].column, - .ui.column.grid > [class*="ten wide tablet"].column { - width: 62.5% !important; - } - - .ui.grid > .row > [class*="eleven wide tablet"].column, - .ui.grid > .column.row > [class*="eleven wide tablet"].column, - .ui.grid > [class*="eleven wide tablet"].column, - .ui.column.grid > [class*="eleven wide tablet"].column { - width: 68.75% !important; - } - - .ui.grid > .row > [class*="twelve wide tablet"].column, - .ui.grid > .column.row > [class*="twelve wide tablet"].column, - .ui.grid > [class*="twelve wide tablet"].column, - .ui.column.grid > [class*="twelve wide tablet"].column { - width: 75% !important; - } - - .ui.grid > .row > [class*="thirteen wide tablet"].column, - .ui.grid > .column.row > [class*="thirteen wide tablet"].column, - .ui.grid > [class*="thirteen wide tablet"].column, - .ui.column.grid > [class*="thirteen wide tablet"].column { - width: 81.25% !important; - } - - .ui.grid > .row > [class*="fourteen wide tablet"].column, - .ui.grid > .column.row > [class*="fourteen wide tablet"].column, - .ui.grid > [class*="fourteen wide tablet"].column, - .ui.column.grid > [class*="fourteen wide tablet"].column { - width: 87.5% !important; - } - - .ui.grid > .row > [class*="fifteen wide tablet"].column, - .ui.grid > .column.row > [class*="fifteen wide tablet"].column, - .ui.grid > [class*="fifteen wide tablet"].column, - .ui.column.grid > [class*="fifteen wide tablet"].column { - width: 93.75% !important; - } - - .ui.grid > .row > [class*="sixteen wide tablet"].column, - .ui.grid > .column.row > [class*="sixteen wide tablet"].column, - .ui.grid > [class*="sixteen wide tablet"].column, - .ui.column.grid > [class*="sixteen wide tablet"].column { - width: 100% !important; - } -} - -/* Computer/Desktop Sizing Combinations */ - -@media only screen and (min-width: 992px) { - .ui.grid > .row > [class*="one wide computer"].column, - .ui.grid > .column.row > [class*="one wide computer"].column, - .ui.grid > [class*="one wide computer"].column, - .ui.column.grid > [class*="one wide computer"].column { - width: 6.25% !important; - } - - .ui.grid > .row > [class*="two wide computer"].column, - .ui.grid > .column.row > [class*="two wide computer"].column, - .ui.grid > [class*="two wide computer"].column, - .ui.column.grid > [class*="two wide computer"].column { - width: 12.5% !important; - } - - .ui.grid > .row > [class*="three wide computer"].column, - .ui.grid > .column.row > [class*="three wide computer"].column, - .ui.grid > [class*="three wide computer"].column, - .ui.column.grid > [class*="three wide computer"].column { - width: 18.75% !important; - } - - .ui.grid > .row > [class*="four wide computer"].column, - .ui.grid > .column.row > [class*="four wide computer"].column, - .ui.grid > [class*="four wide computer"].column, - .ui.column.grid > [class*="four wide computer"].column { - width: 25% !important; - } - - .ui.grid > .row > [class*="five wide computer"].column, - .ui.grid > .column.row > [class*="five wide computer"].column, - .ui.grid > [class*="five wide computer"].column, - .ui.column.grid > [class*="five wide computer"].column { - width: 31.25% !important; - } - - .ui.grid > .row > [class*="six wide computer"].column, - .ui.grid > .column.row > [class*="six wide computer"].column, - .ui.grid > [class*="six wide computer"].column, - .ui.column.grid > [class*="six wide computer"].column { - width: 37.5% !important; - } - - .ui.grid > .row > [class*="seven wide computer"].column, - .ui.grid > .column.row > [class*="seven wide computer"].column, - .ui.grid > [class*="seven wide computer"].column, - .ui.column.grid > [class*="seven wide computer"].column { - width: 43.75% !important; - } - - .ui.grid > .row > [class*="eight wide computer"].column, - .ui.grid > .column.row > [class*="eight wide computer"].column, - .ui.grid > [class*="eight wide computer"].column, - .ui.column.grid > [class*="eight wide computer"].column { - width: 50% !important; - } - - .ui.grid > .row > [class*="nine wide computer"].column, - .ui.grid > .column.row > [class*="nine wide computer"].column, - .ui.grid > [class*="nine wide computer"].column, - .ui.column.grid > [class*="nine wide computer"].column { - width: 56.25% !important; - } - - .ui.grid > .row > [class*="ten wide computer"].column, - .ui.grid > .column.row > [class*="ten wide computer"].column, - .ui.grid > [class*="ten wide computer"].column, - .ui.column.grid > [class*="ten wide computer"].column { - width: 62.5% !important; - } - - .ui.grid > .row > [class*="eleven wide computer"].column, - .ui.grid > .column.row > [class*="eleven wide computer"].column, - .ui.grid > [class*="eleven wide computer"].column, - .ui.column.grid > [class*="eleven wide computer"].column { - width: 68.75% !important; - } - - .ui.grid > .row > [class*="twelve wide computer"].column, - .ui.grid > .column.row > [class*="twelve wide computer"].column, - .ui.grid > [class*="twelve wide computer"].column, - .ui.column.grid > [class*="twelve wide computer"].column { - width: 75% !important; - } - - .ui.grid > .row > [class*="thirteen wide computer"].column, - .ui.grid > .column.row > [class*="thirteen wide computer"].column, - .ui.grid > [class*="thirteen wide computer"].column, - .ui.column.grid > [class*="thirteen wide computer"].column { - width: 81.25% !important; - } - - .ui.grid > .row > [class*="fourteen wide computer"].column, - .ui.grid > .column.row > [class*="fourteen wide computer"].column, - .ui.grid > [class*="fourteen wide computer"].column, - .ui.column.grid > [class*="fourteen wide computer"].column { - width: 87.5% !important; - } - - .ui.grid > .row > [class*="fifteen wide computer"].column, - .ui.grid > .column.row > [class*="fifteen wide computer"].column, - .ui.grid > [class*="fifteen wide computer"].column, - .ui.column.grid > [class*="fifteen wide computer"].column { - width: 93.75% !important; - } - - .ui.grid > .row > [class*="sixteen wide computer"].column, - .ui.grid > .column.row > [class*="sixteen wide computer"].column, - .ui.grid > [class*="sixteen wide computer"].column, - .ui.column.grid > [class*="sixteen wide computer"].column { - width: 100% !important; - } -} - -/* Large Monitor Sizing Combinations */ - -@media only screen and (min-width: 1200px) and (max-width: 1919.98px) { - .ui.grid > .row > [class*="one wide large screen"].column, - .ui.grid > .column.row > [class*="one wide large screen"].column, - .ui.grid > [class*="one wide large screen"].column, - .ui.column.grid > [class*="one wide large screen"].column { - width: 6.25% !important; - } - - .ui.grid > .row > [class*="two wide large screen"].column, - .ui.grid > .column.row > [class*="two wide large screen"].column, - .ui.grid > [class*="two wide large screen"].column, - .ui.column.grid > [class*="two wide large screen"].column { - width: 12.5% !important; - } - - .ui.grid > .row > [class*="three wide large screen"].column, - .ui.grid > .column.row > [class*="three wide large screen"].column, - .ui.grid > [class*="three wide large screen"].column, - .ui.column.grid > [class*="three wide large screen"].column { - width: 18.75% !important; - } - - .ui.grid > .row > [class*="four wide large screen"].column, - .ui.grid > .column.row > [class*="four wide large screen"].column, - .ui.grid > [class*="four wide large screen"].column, - .ui.column.grid > [class*="four wide large screen"].column { - width: 25% !important; - } - - .ui.grid > .row > [class*="five wide large screen"].column, - .ui.grid > .column.row > [class*="five wide large screen"].column, - .ui.grid > [class*="five wide large screen"].column, - .ui.column.grid > [class*="five wide large screen"].column { - width: 31.25% !important; - } - - .ui.grid > .row > [class*="six wide large screen"].column, - .ui.grid > .column.row > [class*="six wide large screen"].column, - .ui.grid > [class*="six wide large screen"].column, - .ui.column.grid > [class*="six wide large screen"].column { - width: 37.5% !important; - } - - .ui.grid > .row > [class*="seven wide large screen"].column, - .ui.grid > .column.row > [class*="seven wide large screen"].column, - .ui.grid > [class*="seven wide large screen"].column, - .ui.column.grid > [class*="seven wide large screen"].column { - width: 43.75% !important; - } - - .ui.grid > .row > [class*="eight wide large screen"].column, - .ui.grid > .column.row > [class*="eight wide large screen"].column, - .ui.grid > [class*="eight wide large screen"].column, - .ui.column.grid > [class*="eight wide large screen"].column { - width: 50% !important; - } - - .ui.grid > .row > [class*="nine wide large screen"].column, - .ui.grid > .column.row > [class*="nine wide large screen"].column, - .ui.grid > [class*="nine wide large screen"].column, - .ui.column.grid > [class*="nine wide large screen"].column { - width: 56.25% !important; - } - - .ui.grid > .row > [class*="ten wide large screen"].column, - .ui.grid > .column.row > [class*="ten wide large screen"].column, - .ui.grid > [class*="ten wide large screen"].column, - .ui.column.grid > [class*="ten wide large screen"].column { - width: 62.5% !important; - } - - .ui.grid > .row > [class*="eleven wide large screen"].column, - .ui.grid > .column.row > [class*="eleven wide large screen"].column, - .ui.grid > [class*="eleven wide large screen"].column, - .ui.column.grid > [class*="eleven wide large screen"].column { - width: 68.75% !important; - } - - .ui.grid > .row > [class*="twelve wide large screen"].column, - .ui.grid > .column.row > [class*="twelve wide large screen"].column, - .ui.grid > [class*="twelve wide large screen"].column, - .ui.column.grid > [class*="twelve wide large screen"].column { - width: 75% !important; - } - - .ui.grid > .row > [class*="thirteen wide large screen"].column, - .ui.grid > .column.row > [class*="thirteen wide large screen"].column, - .ui.grid > [class*="thirteen wide large screen"].column, - .ui.column.grid > [class*="thirteen wide large screen"].column { - width: 81.25% !important; - } - - .ui.grid > .row > [class*="fourteen wide large screen"].column, - .ui.grid > .column.row > [class*="fourteen wide large screen"].column, - .ui.grid > [class*="fourteen wide large screen"].column, - .ui.column.grid > [class*="fourteen wide large screen"].column { - width: 87.5% !important; - } - - .ui.grid > .row > [class*="fifteen wide large screen"].column, - .ui.grid > .column.row > [class*="fifteen wide large screen"].column, - .ui.grid > [class*="fifteen wide large screen"].column, - .ui.column.grid > [class*="fifteen wide large screen"].column { - width: 93.75% !important; - } - - .ui.grid > .row > [class*="sixteen wide large screen"].column, - .ui.grid > .column.row > [class*="sixteen wide large screen"].column, - .ui.grid > [class*="sixteen wide large screen"].column, - .ui.column.grid > [class*="sixteen wide large screen"].column { - width: 100% !important; - } -} - -/* Widescreen Sizing Combinations */ - -@media only screen and (min-width: 1920px) { - .ui.grid > .row > [class*="one wide widescreen"].column, - .ui.grid > .column.row > [class*="one wide widescreen"].column, - .ui.grid > [class*="one wide widescreen"].column, - .ui.column.grid > [class*="one wide widescreen"].column { - width: 6.25% !important; - } - - .ui.grid > .row > [class*="two wide widescreen"].column, - .ui.grid > .column.row > [class*="two wide widescreen"].column, - .ui.grid > [class*="two wide widescreen"].column, - .ui.column.grid > [class*="two wide widescreen"].column { - width: 12.5% !important; - } - - .ui.grid > .row > [class*="three wide widescreen"].column, - .ui.grid > .column.row > [class*="three wide widescreen"].column, - .ui.grid > [class*="three wide widescreen"].column, - .ui.column.grid > [class*="three wide widescreen"].column { - width: 18.75% !important; - } - - .ui.grid > .row > [class*="four wide widescreen"].column, - .ui.grid > .column.row > [class*="four wide widescreen"].column, - .ui.grid > [class*="four wide widescreen"].column, - .ui.column.grid > [class*="four wide widescreen"].column { - width: 25% !important; - } - - .ui.grid > .row > [class*="five wide widescreen"].column, - .ui.grid > .column.row > [class*="five wide widescreen"].column, - .ui.grid > [class*="five wide widescreen"].column, - .ui.column.grid > [class*="five wide widescreen"].column { - width: 31.25% !important; - } - - .ui.grid > .row > [class*="six wide widescreen"].column, - .ui.grid > .column.row > [class*="six wide widescreen"].column, - .ui.grid > [class*="six wide widescreen"].column, - .ui.column.grid > [class*="six wide widescreen"].column { - width: 37.5% !important; - } - - .ui.grid > .row > [class*="seven wide widescreen"].column, - .ui.grid > .column.row > [class*="seven wide widescreen"].column, - .ui.grid > [class*="seven wide widescreen"].column, - .ui.column.grid > [class*="seven wide widescreen"].column { - width: 43.75% !important; - } - - .ui.grid > .row > [class*="eight wide widescreen"].column, - .ui.grid > .column.row > [class*="eight wide widescreen"].column, - .ui.grid > [class*="eight wide widescreen"].column, - .ui.column.grid > [class*="eight wide widescreen"].column { - width: 50% !important; - } - - .ui.grid > .row > [class*="nine wide widescreen"].column, - .ui.grid > .column.row > [class*="nine wide widescreen"].column, - .ui.grid > [class*="nine wide widescreen"].column, - .ui.column.grid > [class*="nine wide widescreen"].column { - width: 56.25% !important; - } - - .ui.grid > .row > [class*="ten wide widescreen"].column, - .ui.grid > .column.row > [class*="ten wide widescreen"].column, - .ui.grid > [class*="ten wide widescreen"].column, - .ui.column.grid > [class*="ten wide widescreen"].column { - width: 62.5% !important; - } - - .ui.grid > .row > [class*="eleven wide widescreen"].column, - .ui.grid > .column.row > [class*="eleven wide widescreen"].column, - .ui.grid > [class*="eleven wide widescreen"].column, - .ui.column.grid > [class*="eleven wide widescreen"].column { - width: 68.75% !important; - } - - .ui.grid > .row > [class*="twelve wide widescreen"].column, - .ui.grid > .column.row > [class*="twelve wide widescreen"].column, - .ui.grid > [class*="twelve wide widescreen"].column, - .ui.column.grid > [class*="twelve wide widescreen"].column { - width: 75% !important; - } - - .ui.grid > .row > [class*="thirteen wide widescreen"].column, - .ui.grid > .column.row > [class*="thirteen wide widescreen"].column, - .ui.grid > [class*="thirteen wide widescreen"].column, - .ui.column.grid > [class*="thirteen wide widescreen"].column { - width: 81.25% !important; - } - - .ui.grid > .row > [class*="fourteen wide widescreen"].column, - .ui.grid > .column.row > [class*="fourteen wide widescreen"].column, - .ui.grid > [class*="fourteen wide widescreen"].column, - .ui.column.grid > [class*="fourteen wide widescreen"].column { - width: 87.5% !important; - } - - .ui.grid > .row > [class*="fifteen wide widescreen"].column, - .ui.grid > .column.row > [class*="fifteen wide widescreen"].column, - .ui.grid > [class*="fifteen wide widescreen"].column, - .ui.column.grid > [class*="fifteen wide widescreen"].column { - width: 93.75% !important; - } - - .ui.grid > .row > [class*="sixteen wide widescreen"].column, - .ui.grid > .column.row > [class*="sixteen wide widescreen"].column, - .ui.grid > [class*="sixteen wide widescreen"].column, - .ui.column.grid > [class*="sixteen wide widescreen"].column { - width: 100% !important; - } -} - -/*---------------------- - Centered - -----------------------*/ - -.ui.centered.grid, -.ui.centered.grid > .row, -.ui.grid > .centered.row { - text-align: center; - justify-content: center; -} - -.ui.centered.grid > .column:not(.aligned):not(.justified):not(.row), -.ui.centered.grid > .row > .column:not(.aligned):not(.justified), -.ui.grid .centered.row > .column:not(.aligned):not(.justified) { - text-align: left; -} - -.ui.grid > .centered.column, -.ui.grid > .row > .centered.column { - display: block; - margin-left: auto; - margin-right: auto; -} - -/*---------------------- - Relaxed - -----------------------*/ - -.ui.relaxed.grid > .column:not(.row), -.ui.relaxed.grid > .row > .column, -.ui.grid > .relaxed.row > .column { - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -.ui[class*="very relaxed"].grid > .column:not(.row), -.ui[class*="very relaxed"].grid > .row > .column, -.ui.grid > [class*="very relaxed"].row > .column { - padding-left: 2.5rem; - padding-right: 2.5rem; -} - -/* Coupling with UI Divider */ - -.ui.relaxed.grid .row + .ui.divider, -.ui.grid .relaxed.row + .ui.divider { - margin-left: 1.5rem; - margin-right: 1.5rem; -} - -.ui[class*="very relaxed"].grid .row + .ui.divider, -.ui.grid [class*="very relaxed"].row + .ui.divider { - margin-left: 2.5rem; - margin-right: 2.5rem; -} - -/*---------------------- - Padded - -----------------------*/ - -.ui.padded.grid:not(.vertically):not(.horizontally) { - margin: 0 !important; -} - -[class*="horizontally padded"].ui.grid { - margin-left: 0 !important; - margin-right: 0 !important; -} - -[class*="vertically padded"].ui.grid { - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -/*---------------------- - "Floated" - -----------------------*/ - -.ui.grid [class*="left floated"].column { - margin-right: auto; -} - -.ui.grid [class*="right floated"].column { - margin-left: auto; -} - -/*---------------------- - Divided - -----------------------*/ - -.ui.divided.grid:not([class*="vertically divided"]) > .column:not(.row), -.ui.divided.grid:not([class*="vertically divided"]) > .row > .column { - box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15); -} - -/* Swap from padding to margin on columns to have dividers align */ - -.ui[class*="vertically divided"].grid > .column:not(.row), -.ui[class*="vertically divided"].grid > .row > .column { - margin-top: 1rem; - margin-bottom: 1rem; - padding-top: 0; - padding-bottom: 0; -} - -.ui[class*="vertically divided"].grid > .row { - margin-top: 0; - margin-bottom: 0; -} - -/* No divider on first column on row */ - -.ui.divided.grid:not([class*="vertically divided"]) > .column:first-child, -.ui.divided.grid:not([class*="vertically divided"]) > .row > .column:first-child { - box-shadow: none; -} - -/* No space on top of first row */ - -.ui[class*="vertically divided"].grid > .row:first-child > .column { - margin-top: 0; -} - -/* Divided Row */ - -.ui.grid > .divided.row > .column { - box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15); -} - -.ui.grid > .divided.row > .column:first-child { - box-shadow: none; -} - -/* Vertically Divided */ - -.ui[class*="vertically divided"].grid > .row { - position: relative; -} - -.ui[class*="vertically divided"].grid > .row:before { - position: absolute; - content: ""; - top: 0; - left: 0; - width: calc(100% - 2rem); - height: 1px; - margin: 0 1rem; - box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15); -} - -/* Padded Horizontally Divided */ - -[class*="horizontally padded"].ui.divided.grid, -.ui.padded.divided.grid:not(.vertically):not(.horizontally) { - width: 100%; -} - -/* First Row Vertically Divided */ - -.ui[class*="vertically divided"].grid > .row:first-child:before { - box-shadow: none; -} - -/* Relaxed */ - -.ui.relaxed[class*="vertically divided"].grid > .row:before { - margin-left: 1.5rem; - margin-right: 1.5rem; - width: calc(100% - 3rem); -} - -.ui[class*="very relaxed"][class*="vertically divided"].grid > .row:before { - margin-left: 2.5rem; - margin-right: 2.5rem; - width: calc(100% - 5rem); -} - -/*---------------------- - Celled - -----------------------*/ - -.ui.celled.grid { - width: 100%; - margin: 1em 0; - box-shadow: 0 0 0 1px #D4D4D5; -} - -.ui.celled.grid > .row { - width: 100% !important; - margin: 0; - padding: 0; - box-shadow: 0 -1px 0 0 #D4D4D5; -} - -.ui.celled.grid > .column:not(.row), -.ui.celled.grid > .row > .column { - box-shadow: -1px 0 0 0 #D4D4D5; -} - -.ui.celled.grid > .column:first-child, -.ui.celled.grid > .row > .column:first-child { - box-shadow: none; -} - -.ui.celled.grid > .column:not(.row), -.ui.celled.grid > .row > .column { - padding: 1em; -} - -.ui.relaxed.celled.grid > .column:not(.row), -.ui.relaxed.celled.grid > .row > .column { - padding: 1.5em; -} - -.ui[class*="very relaxed"].celled.grid > .column:not(.row), -.ui[class*="very relaxed"].celled.grid > .row > .column { - padding: 2em; -} - -/* Internally Celled */ - -.ui[class*="internally celled"].grid { - box-shadow: none; - margin: 0; -} - -.ui[class*="internally celled"].grid > .row:first-child { - box-shadow: none; -} - -.ui[class*="internally celled"].grid > .row > .column:first-child { - box-shadow: none; -} - -/*---------------------- - Vertically Aligned - -----------------------*/ - -/* Top Aligned */ - -.ui[class*="top aligned"].grid > .column:not(.row), -.ui[class*="top aligned"].grid > .row > .column, -.ui.grid > [class*="top aligned"].row > .column, -.ui.grid > [class*="top aligned"].column:not(.row), -.ui.grid > .row > [class*="top aligned"].column { - flex-direction: column; - vertical-align: top; - align-self: flex-start !important; -} - -/* Middle Aligned */ - -.ui[class*="middle aligned"].grid > .column:not(.row), -.ui[class*="middle aligned"].grid > .row > .column, -.ui.grid > [class*="middle aligned"].row > .column, -.ui.grid > [class*="middle aligned"].column:not(.row), -.ui.grid > .row > [class*="middle aligned"].column { - flex-direction: column; - vertical-align: middle; - align-self: center !important; -} - -/* Bottom Aligned */ - -.ui[class*="bottom aligned"].grid > .column:not(.row), -.ui[class*="bottom aligned"].grid > .row > .column, -.ui.grid > [class*="bottom aligned"].row > .column, -.ui.grid > [class*="bottom aligned"].column:not(.row), -.ui.grid > .row > [class*="bottom aligned"].column { - flex-direction: column; - vertical-align: bottom; - align-self: flex-end !important; -} - -/* Stretched */ - -.ui.stretched.grid > .row > .column, -.ui.stretched.grid > .column, -.ui.grid > .stretched.row > .column, -.ui.grid > .stretched.column:not(.row), -.ui.grid > .row > .stretched.column { - display: inline-flex !important; - align-self: stretch; - flex-direction: column; -} - -.ui.stretched.grid > .row > .column > *, -.ui.stretched.grid > .column > *, -.ui.grid > .stretched.row > .column > *, -.ui.grid > .stretched.column:not(.row) > *, -.ui.grid > .row > .stretched.column > * { - flex-grow: 1; -} - -/*---------------------- - Horizontally Centered - -----------------------*/ - -/* Left Aligned */ - -.ui[class*="left aligned"].grid > .column, -.ui[class*="left aligned"].grid > .row > .column, -.ui.grid > [class*="left aligned"].row > .column, -.ui.grid > [class*="left aligned"].column.column, -.ui.grid > .row > [class*="left aligned"].column.column { - text-align: left; - align-self: inherit; -} - -/* Center Aligned */ - -.ui[class*="center aligned"].grid > .column, -.ui[class*="center aligned"].grid > .row > .column, -.ui.grid > [class*="center aligned"].row > .column, -.ui.grid > [class*="center aligned"].column.column, -.ui.grid > .row > [class*="center aligned"].column.column { - text-align: center; - align-self: inherit; -} - -.ui[class*="center aligned"].grid { - justify-content: center; -} - -/* Right Aligned */ - -.ui[class*="right aligned"].grid > .column, -.ui[class*="right aligned"].grid > .row > .column, -.ui.grid > [class*="right aligned"].row > .column, -.ui.grid > [class*="right aligned"].column.column, -.ui.grid > .row > [class*="right aligned"].column.column { - text-align: right; - align-self: inherit; -} - -/* Justified */ - -.ui.justified.grid > .column, -.ui.justified.grid > .row > .column, -.ui.grid > .justified.row > .column, -.ui.grid > .justified.column.column, -.ui.grid > .row > .justified.column.column { - text-align: justify; - -webkit-hyphens: auto; - hyphens: auto; -} - -/*---------------------- - Colored ------------------------*/ - -.ui.grid > .primary.row, -.ui.grid > .primary.column, -.ui.grid > .row > .primary.column { - background-color: #2185D0; - color: #FFFFFF; -} - -.ui.grid > .secondary.row, -.ui.grid > .secondary.column, -.ui.grid > .row > .secondary.column { - background-color: #1B1C1D; - color: #FFFFFF; -} - -.ui.grid > .red.row, -.ui.grid > .red.column, -.ui.grid > .row > .red.column { - background-color: #DB2828; - color: #FFFFFF; -} - -.ui.grid > .orange.row, -.ui.grid > .orange.column, -.ui.grid > .row > .orange.column { - background-color: #F2711C; - color: #FFFFFF; -} - -.ui.grid > .yellow.row, -.ui.grid > .yellow.column, -.ui.grid > .row > .yellow.column { - background-color: #FBBD08; - color: #FFFFFF; -} - -.ui.grid > .olive.row, -.ui.grid > .olive.column, -.ui.grid > .row > .olive.column { - background-color: #B5CC18; - color: #FFFFFF; -} - -.ui.grid > .green.row, -.ui.grid > .green.column, -.ui.grid > .row > .green.column { - background-color: #21BA45; - color: #FFFFFF; -} - -.ui.grid > .teal.row, -.ui.grid > .teal.column, -.ui.grid > .row > .teal.column { - background-color: #00B5AD; - color: #FFFFFF; -} - -.ui.grid > .blue.row, -.ui.grid > .blue.column, -.ui.grid > .row > .blue.column { - background-color: #2185D0; - color: #FFFFFF; -} - -.ui.grid > .violet.row, -.ui.grid > .violet.column, -.ui.grid > .row > .violet.column { - background-color: #6435C9; - color: #FFFFFF; -} - -.ui.grid > .purple.row, -.ui.grid > .purple.column, -.ui.grid > .row > .purple.column { - background-color: #A333C8; - color: #FFFFFF; -} - -.ui.grid > .pink.row, -.ui.grid > .pink.column, -.ui.grid > .row > .pink.column { - background-color: #E03997; - color: #FFFFFF; -} - -.ui.grid > .brown.row, -.ui.grid > .brown.column, -.ui.grid > .row > .brown.column { - background-color: #A5673F; - color: #FFFFFF; -} - -.ui.grid > .grey.row, -.ui.grid > .grey.column, -.ui.grid > .row > .grey.column { - background-color: #767676; - color: #FFFFFF; -} - -.ui.grid > .black.row, -.ui.grid > .black.column, -.ui.grid > .row > .black.column { - background-color: #1B1C1D; - color: #FFFFFF; -} - -/*---------------------- - Equal Width ------------------------*/ - -.ui[class*="equal width"].grid > .column:not(.row), -.ui[class*="equal width"].grid > .row > .column, -.ui.grid > [class*="equal width"].row > .column { - display: inline-block; - flex-grow: 1; -} - -.ui[class*="equal width"].grid > .wide.column, -.ui[class*="equal width"].grid > .row > .wide.column, -.ui.grid > [class*="equal width"].row > .wide.column { - flex-grow: 0; -} - -/*---------------------- - Reverse - -----------------------*/ - -/* Mobile */ - -@media only screen and (max-width: 767.98px) { - .ui[class*="mobile reversed"].grid, - .ui[class*="mobile reversed"].grid > .row, - .ui.grid > [class*="mobile reversed"].row { - flex-direction: row-reverse; - } - - .ui[class*="mobile vertically reversed"].grid, - .ui.stackable[class*="mobile reversed"] { - flex-direction: column-reverse; - } - - /* Divided Reversed */ - - .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child, - .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child { - box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15); - } - - .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child, - .ui[class*="mobile reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child { - box-shadow: none; - } - - /* Vertically Divided Reversed */ - - .ui.grid[class*="vertically divided"][class*="mobile vertically reversed"] > .row:first-child:before { - box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15); - } - - .ui.grid[class*="vertically divided"][class*="mobile vertically reversed"] > .row:last-child:before { - box-shadow: none; - } - - /* Celled Reversed */ - - .ui[class*="mobile reversed"].celled.grid > .row > .column:first-child { - box-shadow: -1px 0 0 0 #D4D4D5; - } - - .ui[class*="mobile reversed"].celled.grid > .row > .column:last-child { - box-shadow: none; - } -} - -/* Tablet */ - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui[class*="tablet reversed"].grid, - .ui[class*="tablet reversed"].grid > .row, - .ui.grid > [class*="tablet reversed"].row { - flex-direction: row-reverse; - } - - .ui[class*="tablet vertically reversed"].grid { - flex-direction: column-reverse; - } - - /* Divided Reversed */ - - .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child, - .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child { - box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15); - } - - .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child, - .ui[class*="tablet reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child { - box-shadow: none; - } - - /* Vertically Divided Reversed */ - - .ui.grid[class*="vertically divided"][class*="tablet vertically reversed"] > .row:first-child:before { - box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15); - } - - .ui.grid[class*="vertically divided"][class*="tablet vertically reversed"] > .row:last-child:before { - box-shadow: none; - } - - /* Celled Reversed */ - - .ui[class*="tablet reversed"].celled.grid > .row > .column:first-child { - box-shadow: -1px 0 0 0 #D4D4D5; - } - - .ui[class*="tablet reversed"].celled.grid > .row > .column:last-child { - box-shadow: none; - } -} - -/* Computer */ - -@media only screen and (min-width: 992px) { - .ui[class*="computer reversed"].grid, - .ui[class*="computer reversed"].grid > .row, - .ui.grid > [class*="computer reversed"].row { - flex-direction: row-reverse; - } - - .ui[class*="computer vertically reversed"].grid { - flex-direction: column-reverse; - } - - /* Divided Reversed */ - - .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .column:first-child, - .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:first-child { - box-shadow: -1px 0 0 0 rgba(34, 36, 38, 0.15); - } - - .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .column:last-child, - .ui[class*="computer reversed"].divided.grid:not([class*="vertically divided"]) > .row > .column:last-child { - box-shadow: none; - } - - /* Vertically Divided Reversed */ - - .ui.grid[class*="vertically divided"][class*="computer vertically reversed"] > .row:first-child:before { - box-shadow: 0 -1px 0 0 rgba(34, 36, 38, 0.15); - } - - .ui.grid[class*="vertically divided"][class*="computer vertically reversed"] > .row:last-child:before { - box-shadow: none; - } - - /* Celled Reversed */ - - .ui[class*="computer reversed"].celled.grid > .row > .column:first-child { - box-shadow: -1px 0 0 0 #D4D4D5; - } - - .ui[class*="computer reversed"].celled.grid > .row > .column:last-child { - box-shadow: none; - } -} - -/*------------------- - Stackable - --------------------*/ - -@media only screen and (max-width: 767.98px) { - .ui.stackable.grid { - width: auto; - margin-left: 0 !important; - margin-right: 0 !important; - } - - .ui.stackable.grid > .row > .wide.column, - .ui.stackable.grid > .wide.column, - .ui.stackable.grid > .column.grid > .column, - .ui.stackable.grid > .column.row > .column, - .ui.stackable.grid > .row > .column, - .ui.stackable.grid > .column:not(.row), - .ui.grid > .stackable.stackable.stackable.row > .column { - width: 100% !important; - margin: 0 0 !important; - box-shadow: none !important; - padding: 1rem 1rem; - } - - .ui.stackable.grid:not(.vertically) > .row { - margin: 0; - padding: 0; - } - - /* Coupling */ - - .ui.container > .ui.stackable.grid > .column, - .ui.container > .ui.stackable.grid > .row > .column { - padding-left: 0 !important; - padding-right: 0 !important; - } - - /* Don't pad inside segment or nested grid */ - - .ui.grid .ui.stackable.grid, - .ui.segment:not(.vertical) .ui.stackable.page.grid { - margin-left: -1rem !important; - margin-right: -1rem !important; - } - - /* Divided Stackable */ - - .ui.stackable.divided.grid > .row:first-child > .column:first-child, - .ui.stackable.celled.grid > .row:first-child > .column:first-child, - .ui.stackable.divided.grid > .column:not(.row):first-child, - .ui.stackable.celled.grid > .column:not(.row):first-child { - border-top: none !important; - } - - .ui.stackable.celled.grid > .column:not(.row), - .ui.stackable.divided:not(.vertically).grid > .column:not(.row), - .ui.stackable.celled.grid > .row > .column, - .ui.stackable.divided:not(.vertically).grid > .row > .column { - border-top: 1px solid rgba(34, 36, 38, 0.15); - box-shadow: none !important; - padding-top: 2rem !important; - padding-bottom: 2rem !important; - } - - .ui.stackable.celled.grid > .row { - box-shadow: none !important; - } - - .ui.stackable.divided:not(.vertically).grid > .column:not(.row), - .ui.stackable.divided:not(.vertically).grid > .row > .column { - padding-left: 0 !important; - padding-right: 0 !important; - } -} - -/*---------------------- - Only (Device) ------------------------*/ - -/* These include arbitrary class repetitions for forced specificity */ - -/* Mobile Only Hide */ - -@media only screen and (max-width: 767.98px) { - .ui[class*="tablet only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="tablet only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="tablet only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.mobile) { - display: none !important; - } - - .ui[class*="computer only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="computer only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="computer only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="computer only"].column:not(.mobile) { - display: none !important; - } - - .ui[class*="large screen only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) { - display: none !important; - } - - .ui[class*="widescreen only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) { - display: none !important; - } -} - -/* Tablet Only Hide */ - -@media only screen and (min-width: 768px) and (max-width: 991.98px) { - .ui[class*="mobile only"].grid.grid.grid:not(.tablet), - .ui.grid.grid.grid > [class*="mobile only"].row:not(.tablet), - .ui.grid.grid.grid > [class*="mobile only"].column:not(.tablet), - .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.tablet) { - display: none !important; - } - - .ui[class*="computer only"].grid.grid.grid:not(.tablet), - .ui.grid.grid.grid > [class*="computer only"].row:not(.tablet), - .ui.grid.grid.grid > [class*="computer only"].column:not(.tablet), - .ui.grid.grid.grid > .row > [class*="computer only"].column:not(.tablet) { - display: none !important; - } - - .ui[class*="large screen only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) { - display: none !important; - } - - .ui[class*="widescreen only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) { - display: none !important; - } -} - -/* Computer Only Hide */ - -@media only screen and (min-width: 992px) and (max-width: 1199.98px) { - .ui[class*="mobile only"].grid.grid.grid:not(.computer), - .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer), - .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer), - .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) { - display: none !important; - } - - .ui[class*="tablet only"].grid.grid.grid:not(.computer), - .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer), - .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer), - .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) { - display: none !important; - } - - .ui[class*="large screen only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="large screen only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="large screen only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="large screen only"].column:not(.mobile) { - display: none !important; - } - - .ui[class*="widescreen only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) { - display: none !important; - } -} - -/* Large Screen Only Hide */ - -@media only screen and (min-width: 1200px) and (max-width: 1919.98px) { - .ui[class*="mobile only"].grid.grid.grid:not(.computer), - .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer), - .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer), - .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) { - display: none !important; - } - - .ui[class*="tablet only"].grid.grid.grid:not(.computer), - .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer), - .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer), - .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) { - display: none !important; - } - - .ui[class*="widescreen only"].grid.grid.grid:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].row:not(.mobile), - .ui.grid.grid.grid > [class*="widescreen only"].column:not(.mobile), - .ui.grid.grid.grid > .row > [class*="widescreen only"].column:not(.mobile) { - display: none !important; - } -} - -/* Widescreen Only Hide */ - -@media only screen and (min-width: 1920px) { - .ui[class*="mobile only"].grid.grid.grid:not(.computer), - .ui.grid.grid.grid > [class*="mobile only"].row:not(.computer), - .ui.grid.grid.grid > [class*="mobile only"].column:not(.computer), - .ui.grid.grid.grid > .row > [class*="mobile only"].column:not(.computer) { - display: none !important; - } - - .ui[class*="tablet only"].grid.grid.grid:not(.computer), - .ui.grid.grid.grid > [class*="tablet only"].row:not(.computer), - .ui.grid.grid.grid > [class*="tablet only"].column:not(.computer), - .ui.grid.grid.grid > .row > [class*="tablet only"].column:not(.computer) { - display: none !important; - } -} - -/*----------------- - Compact - -----------------*/ - -.ui.ui.ui.compact.grid > .column:not(.row), -.ui.ui.ui.compact.grid > .row > .column { - padding-left: 0.5rem; - padding-right: 0.5rem; -} - -.ui.ui.ui.compact.grid > * { - padding-left: 0.5rem; - padding-right: 0.5rem; -} - -/* Row */ - -.ui.ui.ui.compact.grid > .row { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -/* Columns */ - -.ui.ui.ui.compact.grid > .column:not(.row) { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -/* Relaxed + Celled */ - -.ui.compact.relaxed.celled.grid > .column:not(.row), -.ui.compact.relaxed.celled.grid > .row > .column { - padding: 0.75em; -} - -.ui.compact[class*="very relaxed"].celled.grid > .column:not(.row), -.ui.compact[class*="very relaxed"].celled.grid > .row > .column { - padding: 1em; -} - -/*----------------- - Very compact - -----------------*/ - -.ui.ui.ui[class*="very compact"].grid > .column:not(.row), -.ui.ui.ui[class*="very compact"].grid > .row > .column { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -.ui.ui.ui[class*="very compact"].grid > * { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -/* Row */ - -.ui.ui.ui[class*="very compact"].grid > .row { - padding-top: 0.25rem; - padding-bottom: 0.25rem; - padding-left: 0.75rem; - padding-right: 0.75rem; -} - -/* Columns */ - -.ui.ui.ui[class*="very compact"].grid > .column:not(.row) { - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - -/* Relaxed + Celled */ - -.ui[class*="very compact"].relaxed.celled.grid > .column:not(.row), -.ui[class*="very compact"].relaxed.celled.grid > .row > .column { - padding: 0.375em; -} - -.ui[class*="very compact"][class*="very relaxed"].celled.grid > .column:not(.row), -.ui[class*="very compact"][class*="very relaxed"].celled.grid > .row > .column { - padding: 0.5em; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Header - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Header -*******************************/ - -/* Standard */ - -.ui.header { - border: none; - margin: calc(2rem - 0.1428571428571429em) 0 1rem; - padding: 0 0; - font-family: var(--fonts-regular); - font-weight: 500; - line-height: 1.28571429em; - text-transform: none; - color: rgba(0, 0, 0, 0.87); -} - -.ui.header:first-child { - margin-top: -0.14285714em; -} - -.ui.header:last-child { - margin-bottom: 0; -} - -/*-------------- - Sub Header - ---------------*/ - -.ui.header .sub.header { - display: block; - font-weight: normal; - padding: 0; - margin: 0; - font-size: 1rem; - line-height: 1.2em; - color: rgba(0, 0, 0, 0.6); -} - -/*-------------- - Icon ----------------*/ - -.ui.header > i.icon { - display: table-cell; - opacity: 1; - font-size: 1.5em; - padding-top: 0; - vertical-align: middle; -} - -/* With Text Node */ - -.ui.header > i.icon:only-child { - display: inline-block; - padding: 0; - margin-right: 0.75rem; -} - -/*------------------- - Image ---------------------*/ - -.ui.header > .image:not(.icon), -.ui.header > img { - display: inline-block; - margin-top: 0.14285714em; - width: 2.5em; - height: auto; - vertical-align: middle; -} - -.ui.header > .image:not(.icon):only-child, -.ui.header > img:only-child { - margin-right: 0.75rem; -} - -/*-------------- - Content ----------------*/ - -.ui.header .content { - display: inline-block; - vertical-align: top; -} - -/* After Image */ - -.ui.header > img + .content, -.ui.header > .image + .content { - padding-left: 0.75rem; - vertical-align: middle; -} - -/* After Icon */ - -.ui.header > i.icon + .content { - padding-left: 0.75rem; - display: table-cell; - vertical-align: middle; -} - -/*-------------- - Loose Coupling ----------------*/ - -.ui.header .ui.label { - font-size: ''; - margin-left: 0.5rem; - vertical-align: middle; -} - -/* Positioning */ - -.ui.header + p { - margin-top: 0; -} - -/******************************* - Types -*******************************/ - -/*-------------- - Page ----------------*/ - -h1.ui.header { - font-size: 2rem; -} - -h1.ui.header .sub.header { - font-size: 1.14285714rem; -} - -h2.ui.header { - font-size: 1.71428571rem; -} - -h2.ui.header .sub.header { - font-size: 1.14285714rem; -} - -h3.ui.header { - font-size: 1.28571429rem; -} - -h3.ui.header .sub.header { - font-size: 1rem; -} - -h4.ui.header { - font-size: 1.07142857rem; -} - -h4.ui.header .sub.header { - font-size: 1rem; -} - -h5.ui.header { - font-size: 1rem; -} - -h5.ui.header .sub.header { - font-size: 0.92857143rem; -} - -h6.ui.header { - font-size: 0.85714286rem; -} - -h6.ui.header .sub.header { - font-size: 0.92857143rem; -} - -/*-------------- - Content Heading ----------------*/ - -.ui.mini.header { - font-size: 0.85714286em; -} - -.ui.mini.header .sub.header { - font-size: 0.92857143rem; -} - -.ui.mini.sub.header { - font-size: 0.78571429em; -} - -.ui.tiny.header { - font-size: 1em; -} - -.ui.tiny.header .sub.header { - font-size: 0.92857143rem; -} - -.ui.tiny.sub.header { - font-size: 0.78571429em; -} - -.ui.small.header { - font-size: 1.07142857em; -} - -.ui.small.header .sub.header { - font-size: 1rem; -} - -.ui.small.sub.header { - font-size: 0.78571429em; -} - -.ui.large.header { - font-size: 1.71428571em; -} - -.ui.large.header .sub.header { - font-size: 1.14285714rem; -} - -.ui.large.sub.header { - font-size: 0.92857143em; -} - -.ui.big.header { - font-size: 1.85714286em; -} - -.ui.big.header .sub.header { - font-size: 1.14285714rem; -} - -.ui.big.sub.header { - font-size: 1em; -} - -.ui.huge.header { - font-size: 2em; - min-height: 1em; -} - -.ui.huge.header .sub.header { - font-size: 1.14285714rem; -} - -.ui.huge.sub.header { - font-size: 1em; -} - -.ui.massive.header { - font-size: 2.28571429em; - min-height: 1em; -} - -.ui.massive.header .sub.header { - font-size: 1.42857143rem; -} - -.ui.massive.sub.header { - font-size: 1.14285714em; -} - -/*-------------- - Sub Heading - ---------------*/ - -.ui.sub.header { - padding: 0; - margin-bottom: 0.14285714rem; - font-weight: 500; - font-size: 0.85714286em; - text-transform: uppercase; - color: ''; -} - -/*------------------- - Icon - --------------------*/ - -.ui.icon.header { - display: inline-block; - text-align: center; - margin: 2rem 0 1rem; -} - -.ui.icon.header:after { - content: ''; - display: block; - height: 0; - clear: both; - visibility: hidden; -} - -.ui.icon.header:first-child { - margin-top: 0; -} - -.ui.icon.header > i.icon { - float: none; - display: block; - width: auto; - height: auto; - line-height: 1; - padding: 0; - font-size: 3em; - margin: 0 auto 0.5rem; - opacity: 1; -} - -.ui.icon.header .corner.icon { - font-size: calc(3em * 0.45); -} - -.ui.icon.header .content { - display: block; - padding: 0; -} - -.ui.icon.header > i.circular.icon { - font-size: 2em; -} - -.ui.icon.header > i.square.icon { - font-size: 2em; -} - -.ui.block.icon.header > i.icon { - margin-bottom: 0; -} - -.ui.icon.header.aligned { - margin-left: auto; - margin-right: auto; - display: block; -} - -/******************************* - States -*******************************/ - -.ui.disabled.header { - opacity: var(--opacity-disabled); -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Colors ---------------------*/ - -.ui.primary.header { - color: #2185D0; -} - -a.ui.primary.header:hover { - color: #1678c2; -} - -.ui.primary.dividing.header { - border-bottom: 2px solid #2185D0; -} - -.ui.secondary.header { - color: #1B1C1D; -} - -a.ui.secondary.header:hover { - color: #27292a; -} - -.ui.secondary.dividing.header { - border-bottom: 2px solid #1B1C1D; -} - -.ui.red.header { - color: #DB2828; -} - -a.ui.red.header:hover { - color: #d01919; -} - -.ui.red.dividing.header { - border-bottom: 2px solid #DB2828; -} - -.ui.orange.header { - color: #F2711C; -} - -a.ui.orange.header:hover { - color: #f26202; -} - -.ui.orange.dividing.header { - border-bottom: 2px solid #F2711C; -} - -.ui.yellow.header { - color: #FBBD08; -} - -a.ui.yellow.header:hover { - color: #eaae00; -} - -.ui.yellow.dividing.header { - border-bottom: 2px solid #FBBD08; -} - -.ui.olive.header { - color: #B5CC18; -} - -a.ui.olive.header:hover { - color: #a7bd0d; -} - -.ui.olive.dividing.header { - border-bottom: 2px solid #B5CC18; -} - -.ui.green.header { - color: #21BA45; -} - -a.ui.green.header:hover { - color: #16ab39; -} - -.ui.green.dividing.header { - border-bottom: 2px solid #21BA45; -} - -.ui.teal.header { - color: #00B5AD; -} - -a.ui.teal.header:hover { - color: #009c95; -} - -.ui.teal.dividing.header { - border-bottom: 2px solid #00B5AD; -} - -.ui.blue.header { - color: #2185D0; -} - -a.ui.blue.header:hover { - color: #1678c2; -} - -.ui.blue.dividing.header { - border-bottom: 2px solid #2185D0; -} - -.ui.violet.header { - color: #6435C9; -} - -a.ui.violet.header:hover { - color: #5829bb; -} - -.ui.violet.dividing.header { - border-bottom: 2px solid #6435C9; -} - -.ui.purple.header { - color: #A333C8; -} - -a.ui.purple.header:hover { - color: #9627ba; -} - -.ui.purple.dividing.header { - border-bottom: 2px solid #A333C8; -} - -.ui.pink.header { - color: #E03997; -} - -a.ui.pink.header:hover { - color: #e61a8d; -} - -.ui.pink.dividing.header { - border-bottom: 2px solid #E03997; -} - -.ui.brown.header { - color: #A5673F; -} - -a.ui.brown.header:hover { - color: #975b33; -} - -.ui.brown.dividing.header { - border-bottom: 2px solid #A5673F; -} - -.ui.grey.header { - color: #767676; -} - -a.ui.grey.header:hover { - color: #838383; -} - -.ui.grey.dividing.header { - border-bottom: 2px solid #767676; -} - -.ui.black.header { - color: #1B1C1D; -} - -a.ui.black.header:hover { - color: #27292a; -} - -.ui.black.dividing.header { - border-bottom: 2px solid #1B1C1D; -} - -/*------------------- - Aligned - --------------------*/ - -.ui.left.aligned.header { - text-align: left; -} - -.ui.right.aligned.header { - text-align: right; -} - -.ui.centered.header, -.ui.center.aligned.header { - text-align: center; -} - -.ui.justified.header { - text-align: justify; -} - -.ui.justified.header:after { - display: inline-block; - content: ''; - width: 100%; -} - -/*------------------- - Floated - --------------------*/ - -.ui.floated.header, -.ui[class*="left floated"].header { - float: left; - margin-top: 0; - margin-right: 0.5em; -} - -.ui[class*="right floated"].header { - float: right; - margin-top: 0; - margin-left: 0.5em; -} - -/*------------------- - Fitted - --------------------*/ - -.ui.fitted.header { - padding: 0; -} - -/*------------------- - Dividing - --------------------*/ - -.ui.dividing.header { - padding-bottom: 0.21428571rem; - border-bottom: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.dividing.header .sub.header { - padding-bottom: 0.21428571rem; -} - -.ui.dividing.header i.icon { - margin-bottom: 0; -} - -/*------------------- - Block - --------------------*/ - -.ui.block.header { - background: #F3F4F5; - padding: 0.78571429rem 1rem; - box-shadow: none; - border: 1px solid #D4D4D5; - border-radius: 0.28571429rem; -} - -.ui.block.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) { - font-size: 1rem; -} - -.ui.mini.block.header { - font-size: 0.78571429rem; -} - -.ui.tiny.block.header { - font-size: 0.85714286rem; -} - -.ui.small.block.header { - font-size: 0.92857143rem; -} - -.ui.large.block.header { - font-size: 1.14285714rem; -} - -.ui.big.block.header { - font-size: 1.28571429rem; -} - -.ui.huge.block.header { - font-size: 1.42857143rem; -} - -.ui.massive.block.header { - font-size: 1.71428571rem; -} - -/*------------------- - Attached - --------------------*/ - -.ui.attached.header { - background: #FFFFFF; - padding: 0.78571429rem 1rem; - margin: 0 -1px 0 -1px; - box-shadow: none; - border: 1px solid #D4D4D5; - border-radius: 0; -} - -.ui.attached.block.header { - background: #F3F4F5; -} - -.ui.attached:not(.top).header { - border-top: none; -} - -.ui.top.attached.header { - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -.ui.bottom.attached.header { - border-radius: 0 0 0.28571429rem 0.28571429rem; -} - -/* Attached Sizes */ - -.ui.attached.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) { - font-size: 1em; -} - -.ui.mini.attached.header { - font-size: 0.78571429em; -} - -.ui.tiny.attached.header { - font-size: 0.85714286em; -} - -.ui.small.attached.header { - font-size: 0.92857143em; -} - -.ui.large.attached.header { - font-size: 1.14285714em; -} - -.ui.big.attached.header { - font-size: 1.28571429em; -} - -.ui.huge.attached.header { - font-size: 1.42857143em; -} - -.ui.massive.attached.header { - font-size: 1.71428571em; -} - -/*------------------- - Sizing ---------------------*/ - -.ui.header:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) { - font-size: 1.28571429em; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Input - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Standard -*******************************/ - -/*-------------------- - Inputs ----------------------*/ - -.ui.input { - position: relative; - font-weight: normal; - font-style: normal; - display: inline-flex; - color: rgba(0, 0, 0, 0.87); -} - -.ui.input > input { - margin: 0; - max-width: 100%; - flex: 1 0 auto; - outline: none; - -webkit-tap-highlight-color: rgba(255, 255, 255, 0); - text-align: left; - line-height: 1.21428571em; - font-family: var(--fonts-regular); - padding: 0.67857143em 1em; - background: #FFFFFF; - border: 1px solid rgba(34, 36, 38, 0.15); - color: rgba(0, 0, 0, 0.87); - border-radius: 0.28571429rem; - transition: box-shadow 0.1s ease, border-color 0.1s ease; - box-shadow: none; -} - -/*-------------------- - Placeholder ----------------------*/ - -/* browsers require these rules separate */ - -.ui.input > input::-webkit-input-placeholder { - color: rgba(191, 191, 191, 0.87); -} - -.ui.input > input::-moz-placeholder { - color: rgba(191, 191, 191, 0.87); -} - -.ui.input > input:-ms-input-placeholder { - color: rgba(191, 191, 191, 0.87); -} - -/******************************* - States -*******************************/ - -/*-------------------- - Disabled - ---------------------*/ - -.ui.disabled.input, -.ui.input:not(.disabled) input[disabled] { - opacity: var(--opacity-disabled); -} - -.ui.disabled.input > input, -.ui.input:not(.disabled) input[disabled] { - pointer-events: none; -} - -/*-------------------- - Active ----------------------*/ - -.ui.input > input:active, -.ui.input.down input { - border-color: rgba(0, 0, 0, 0.3); - background: #FAFAFA; - color: rgba(0, 0, 0, 0.87); - box-shadow: none; -} - -/*-------------------- - Loading - ---------------------*/ - -.ui.loading.loading.input > i.icon:before { - position: absolute; - content: ''; - top: 50%; - left: 50%; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - border-radius: 500rem; - border: 0.2em solid rgba(0, 0, 0, 0.1); -} - -.ui.loading.loading.input > i.icon:after { - position: absolute; - content: ''; - top: 50%; - left: 50%; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - -webkit-animation: loader 0.6s infinite linear; - animation: loader 0.6s infinite linear; - border: 0.2em solid #767676; - border-radius: 500rem; - box-shadow: 0 0 0 1px transparent; -} - -/*-------------------- - Focus ----------------------*/ - -.ui.input.focus > input, -.ui.input > input:focus { - border-color: #85B7D9; - background: #FFFFFF; - color: rgba(0, 0, 0, 0.8); - box-shadow: none; -} - -.ui.input.focus > input::-webkit-input-placeholder, -.ui.input > input:focus::-webkit-input-placeholder { - color: rgba(115, 115, 115, 0.87); -} - -.ui.input.focus > input::-moz-placeholder, -.ui.input > input:focus::-moz-placeholder { - color: rgba(115, 115, 115, 0.87); -} - -.ui.input.focus > input:-ms-input-placeholder, -.ui.input > input:focus:-ms-input-placeholder { - color: rgba(115, 115, 115, 0.87); -} - -/*-------------------- - States - ---------------------*/ - -.ui.input.error > input { - background-color: #FFF6F6; - border-color: #E0B4B4; - color: #9F3A38; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.error > input::-webkit-input-placeholder { - color: #e7bdbc; -} - -.ui.input.error > input::-moz-placeholder { - color: #e7bdbc; -} - -.ui.input.error > input:-ms-input-placeholder { - color: #e7bdbc !important; -} - -/* Focused Placeholder */ - -.ui.input.error > input:focus::-webkit-input-placeholder { - color: #da9796; -} - -.ui.input.error > input:focus::-moz-placeholder { - color: #da9796; -} - -.ui.input.error > input:focus:-ms-input-placeholder { - color: #da9796 !important; -} - -.ui.input.info > input { - background-color: #F8FFFF; - border-color: #A9D5DE; - color: #276F86; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.info > input::-webkit-input-placeholder { - color: #98cfe1; -} - -.ui.input.info > input::-moz-placeholder { - color: #98cfe1; -} - -.ui.input.info > input:-ms-input-placeholder { - color: #98cfe1 !important; -} - -/* Focused Placeholder */ - -.ui.input.info > input:focus::-webkit-input-placeholder { - color: #70bdd6; -} - -.ui.input.info > input:focus::-moz-placeholder { - color: #70bdd6; -} - -.ui.input.info > input:focus:-ms-input-placeholder { - color: #70bdd6 !important; -} - -.ui.input.success > input { - background-color: #FCFFF5; - border-color: #A3C293; - color: #2C662D; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.success > input::-webkit-input-placeholder { - color: #8fcf90; -} - -.ui.input.success > input::-moz-placeholder { - color: #8fcf90; -} - -.ui.input.success > input:-ms-input-placeholder { - color: #8fcf90 !important; -} - -/* Focused Placeholder */ - -.ui.input.success > input:focus::-webkit-input-placeholder { - color: #6cbf6d; -} - -.ui.input.success > input:focus::-moz-placeholder { - color: #6cbf6d; -} - -.ui.input.success > input:focus:-ms-input-placeholder { - color: #6cbf6d !important; -} - -.ui.input.warning > input { - background-color: #FFFAF3; - border-color: #C9BA9B; - color: #573A08; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.warning > input::-webkit-input-placeholder { - color: #edad3e; -} - -.ui.input.warning > input::-moz-placeholder { - color: #edad3e; -} - -.ui.input.warning > input:-ms-input-placeholder { - color: #edad3e !important; -} - -/* Focused Placeholder */ - -.ui.input.warning > input:focus::-webkit-input-placeholder { - color: #e39715; -} - -.ui.input.warning > input:focus::-moz-placeholder { - color: #e39715; -} - -.ui.input.warning > input:focus:-ms-input-placeholder { - color: #e39715 !important; -} - -/******************************* - Variations -*******************************/ - -/*-------------------- - Transparent - ---------------------*/ - -.ui.transparent.input > textarea, -.ui.transparent.input > input { - border-color: transparent !important; - background-color: transparent !important; - padding: 0; - box-shadow: none !important; - border-radius: 0 !important; -} - -.field .ui.transparent.input > textarea { - padding: 0.67857143em 1em; -} - -/* Transparent Icon */ - -:not(.field) > .ui.transparent.icon.input > i.icon { - width: 1.1em; -} - -:not(.field) > .ui.ui.ui.transparent.icon.input > input { - padding-left: 0; - padding-right: 2em; -} - -:not(.field) > .ui.ui.ui.transparent[class*="left icon"].input > input { - padding-left: 2em; - padding-right: 0; -} - -/*-------------------- - Icon - ---------------------*/ - -.ui.icon.input > i.icon { - cursor: default; - position: absolute; - line-height: 1; - text-align: center; - top: 0; - right: 0; - margin: 0; - height: 100%; - width: 2.67142857em; - opacity: 0.5; - border-radius: 0 0.28571429rem 0.28571429rem 0; - transition: opacity 0.3s ease; -} - -.ui.icon.input > i.icon:not(.link) { - pointer-events: none; -} - -.ui.ui.ui.ui.icon.input > textarea, -.ui.ui.ui.ui.icon.input > input { - padding-right: 2.67142857em; -} - -.ui.icon.input > i.icon:before, -.ui.icon.input > i.icon:after { - left: 0; - position: absolute; - text-align: center; - top: 50%; - width: 100%; - margin-top: -0.5em; -} - -.ui.icon.input > i.link.icon { - cursor: pointer; -} - -.ui.icon.input > i.circular.icon { - top: 0.35em; - right: 0.5em; -} - -/* Left Icon Input */ - -.ui[class*="left icon"].input > i.icon { - right: auto; - left: 1px; - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui[class*="left icon"].input > i.circular.icon { - right: auto; - left: 0.5em; -} - -.ui.ui.ui.ui[class*="left icon"].input > textarea, -.ui.ui.ui.ui[class*="left icon"].input > input { - padding-left: 2.67142857em; - padding-right: 1em; -} - -/* Focus */ - -.ui.icon.input > textarea:focus ~ i.icon, -.ui.icon.input > input:focus ~ i.icon { - opacity: 1; -} - -/*-------------------- - Labeled - ---------------------*/ - -/* Adjacent Label */ - -.ui.labeled.input > .label { - flex: 0 0 auto; - margin: 0; - font-size: 1em; -} - -.ui.labeled.input > .label:not(.corner) { - padding-top: 0.78571429em; - padding-bottom: 0.78571429em; -} - -/* Regular Label on Left */ - -.ui.labeled.input:not([class*="corner labeled"]) .label:first-child { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-color: transparent; -} - -.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input:focus { - border-left-color: #85B7D9; -} - -/* Regular Label on Right */ - -.ui[class*="right labeled"].input > input { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - border-right-color: transparent !important; -} - -.ui[class*="right labeled"].input > input + .label { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.ui[class*="right labeled"].input > input:focus { - border-right-color: #85B7D9 !important; -} - -/* Corner Label */ - -.ui.labeled.input .corner.label { - top: 1px; - right: 1px; - font-size: 0.64285714em; - border-radius: 0 0.28571429rem 0 0; -} - -/* Spacing with corner label */ - -.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > textarea, -.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > input { - padding-right: 2.5em !important; -} - -.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > textarea, -.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > input { - padding-right: 3.25em !important; -} - -.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > i.icon { - margin-right: 1.25em; -} - -/* Left Labeled */ - -.ui[class*="left corner labeled"].labeled.input > textarea, -.ui[class*="left corner labeled"].labeled.input > input { - padding-left: 2.5em !important; -} - -.ui[class*="left corner labeled"].icon.input > textarea, -.ui[class*="left corner labeled"].icon.input > input { - padding-left: 3.25em !important; -} - -.ui[class*="left corner labeled"].icon.input > i.icon { - margin-left: 1.25em; -} - -.ui.icon.input > textarea ~ i.icon { - height: 3em; -} - -:not(.field) > .ui.transparent.icon.input > textarea ~ i.icon { - height: 1.3em; -} - -/* Corner Label Position */ - -.ui.input > .ui.corner.label { - top: 1px; - right: 1px; -} - -.ui.input > .ui.left.corner.label { - right: auto; - left: 1px; -} - -/* Labeled and action input states */ - -.ui.form .field.error > .ui.action.input > .ui.button, -.ui.form .field.error > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.error > .ui.button, -.ui.labeled.input.error:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #E0B4B4; - border-bottom: 1px solid #E0B4B4; -} - -.ui.form .field.error > .ui[class*="left action"].input > .ui.button, -.ui.form .field.error > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.error > .ui.button, -.ui.labeled.input.error:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #E0B4B4; -} - -.ui.form .field.error > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.error:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.error:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #E0B4B4; -} - -.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.error:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #E0B4B4; -} - -.ui.form .field.info > .ui.action.input > .ui.button, -.ui.form .field.info > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.info > .ui.button, -.ui.labeled.input.info:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #A9D5DE; - border-bottom: 1px solid #A9D5DE; -} - -.ui.form .field.info > .ui[class*="left action"].input > .ui.button, -.ui.form .field.info > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.info > .ui.button, -.ui.labeled.input.info:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #A9D5DE; -} - -.ui.form .field.info > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.info:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.info:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #A9D5DE; -} - -.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.info:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #A9D5DE; -} - -.ui.form .field.success > .ui.action.input > .ui.button, -.ui.form .field.success > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.success > .ui.button, -.ui.labeled.input.success:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #A3C293; - border-bottom: 1px solid #A3C293; -} - -.ui.form .field.success > .ui[class*="left action"].input > .ui.button, -.ui.form .field.success > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.success > .ui.button, -.ui.labeled.input.success:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #A3C293; -} - -.ui.form .field.success > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.success:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.success:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #A3C293; -} - -.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.success:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #A3C293; -} - -.ui.form .field.warning > .ui.action.input > .ui.button, -.ui.form .field.warning > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.warning > .ui.button, -.ui.labeled.input.warning:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #C9BA9B; - border-bottom: 1px solid #C9BA9B; -} - -.ui.form .field.warning > .ui[class*="left action"].input > .ui.button, -.ui.form .field.warning > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.warning > .ui.button, -.ui.labeled.input.warning:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #C9BA9B; -} - -.ui.form .field.warning > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.warning:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.warning:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #C9BA9B; -} - -.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.warning:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #C9BA9B; -} - -/*-------------------- - Action - ---------------------*/ - -.ui.action.input > .button, -.ui.action.input > .buttons { - display: flex; - align-items: center; - flex: 0 0 auto; -} - -.ui.action.input > .button, -.ui.action.input > .buttons > .button { - padding-top: 0.78571429em; - padding-bottom: 0.78571429em; - margin: 0; -} - -/* Input when ui Left*/ - -.ui[class*="left action"].input > input { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-color: transparent; -} - -/* Input when ui Right*/ - -.ui.action.input:not([class*="left action"]) > input { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right-color: transparent; -} - -/* Button and Dropdown */ - -.ui.action.input > .dropdown:first-child, -.ui.action.input > .button:first-child, -.ui.action.input > .buttons:first-child > .button { - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui.action.input > .dropdown:not(:first-child), -.ui.action.input > .button:not(:first-child), -.ui.action.input > .buttons:not(:first-child) > .button { - border-radius: 0; -} - -.ui.action.input > .dropdown:last-child, -.ui.action.input > .button:last-child, -.ui.action.input > .buttons:last-child > .button { - border-radius: 0 0.28571429rem 0.28571429rem 0; -} - -/* Input Focus */ - -.ui.action.input:not([class*="left action"]) > input:focus { - border-right-color: #85B7D9; -} - -.ui.ui[class*="left action"].input > input:focus { - border-left-color: #85B7D9; -} - -/*-------------------- - Fluid - ---------------------*/ - -.ui.fluid.input { - display: flex; -} - -.ui.fluid.input > input { - width: 0 !important; -} - -/*-------------------- - Size ----------------------*/ - -.ui.input { - font-size: 1em; -} - -.ui.mini.input { - font-size: 0.78571429em; -} - -.ui.tiny.input { - font-size: 0.85714286em; -} - -.ui.small.input { - font-size: 0.92857143em; -} - -.ui.large.input { - font-size: 1.14285714em; -} - -.ui.big.input { - font-size: 1.28571429em; -} - -.ui.huge.input { - font-size: 1.42857143em; -} - -.ui.massive.input { - font-size: 1.71428571em; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Label - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Label -*******************************/ - -.ui.label { - display: inline-block; - line-height: 1; - vertical-align: baseline; - margin: 0 0.14285714em; - background-color: #E8E8E8; - background-image: none; - padding: 0.5833em 0.833em; - color: rgba(0, 0, 0, 0.6); - text-transform: none; - font-weight: 500; - border: 0 solid transparent; - border-radius: 0.28571429rem; - transition: background 0.1s ease; -} - -.ui.label:first-child { - margin-left: 0; -} - -.ui.label:last-child { - margin-right: 0; -} - -/* Link */ - -a.ui.label { - cursor: pointer; -} - -/* Inside Link */ - -.ui.label > a { - cursor: pointer; - color: inherit; - opacity: 0.5; - transition: 0.1s opacity ease; -} - -.ui.label > a:hover { - opacity: 1; -} - -/* Image */ - -.ui.label > img { - width: auto !important; - vertical-align: middle; - height: 2.1666em; -} - -/* Icon */ - -.ui.left.icon.label > .icon, -.ui.label > .icon { - width: auto; - margin: 0 0.75em 0 0; -} - -/* Detail */ - -.ui.label > .detail { - display: inline-block; - vertical-align: top; - font-weight: 500; - margin-left: 1em; - opacity: 0.8; -} - -.ui.label > .detail .icon { - margin: 0 0.25em 0 0; -} - -/* Removable label */ - -.ui.label > .close.icon, -.ui.label > .delete.icon { - cursor: pointer; - font-size: 0.92857143em; - opacity: 0.5; - transition: background 0.1s ease; -} - -.ui.label > .close.icon:hover, -.ui.label > .delete.icon:hover { - opacity: 1; -} - -/* Backward compatible positioning */ - -.ui.label.left.icon > .close.icon, -.ui.label.left.icon > .delete.icon { - margin: 0 0.5em 0 0; -} - -.ui.label:not(.icon) > .close.icon, -.ui.label:not(.icon) > .delete.icon { - margin: 0 0 0 0.5em; -} - -/* Label for only an icon */ - -.ui.icon.label > .icon { - margin: 0 auto; -} - -/* Right Side Icon */ - -.ui.right.icon.label > .icon { - margin: 0 0 0 0.75em; -} - -/*------------------- - Group ---------------------*/ - -.ui.labels > .label { - margin: 0 0.5em 0.5em 0; -} - -/*------------------- - Coupling ---------------------*/ - -.ui.header > .ui.label { - margin-top: -0.29165em; -} - -/* Remove border radius on attached segment */ - -.ui.attached.segment > .ui.top.left.attached.label, -.ui.bottom.attached.segment > .ui.top.left.attached.label { - border-top-left-radius: 0; -} - -.ui.attached.segment > .ui.top.right.attached.label, -.ui.bottom.attached.segment > .ui.top.right.attached.label { - border-top-right-radius: 0; -} - -.ui.top.attached.segment > .ui.bottom.left.attached.label { - border-bottom-left-radius: 0; -} - -.ui.top.attached.segment > .ui.bottom.right.attached.label { - border-bottom-right-radius: 0; -} - -/* Padding on next content after a label */ - -.ui.top.attached.label ~ .ui.bottom.attached.label + :not(.attached), -.ui.top.attached.label + :not(.attached) { - margin-top: 2rem !important; -} - -.ui.bottom.attached.label ~ :last-child:not(.attached) { - margin-top: 0; - margin-bottom: 2rem !important; -} - -.ui.segment:not(.basic) > .ui.top.attached.label { - margin-top: -1px; -} - -.ui.segment:not(.basic) > .ui.bottom.attached.label { - margin-bottom: -1px; -} - -.ui.segment:not(.basic) > .ui.attached.label:not(.right) { - margin-left: -1px; -} - -.ui.segment:not(.basic) > .ui.right.attached.label { - margin-right: -1px; -} - -.ui.segment:not(.basic) > .ui.attached.label:not(.left):not(.right) { - width: calc(100% + 2px); -} - -/******************************* - Types -*******************************/ - -/*------------------- - Attached - --------------------*/ - -.ui[class*="top attached"].label, -.ui.attached.label { - width: 100%; - position: absolute; - margin: 0; - top: 0; - left: 0; - padding: 0.75em 1em; - border-radius: 0.21428571rem 0.21428571rem 0 0; -} - -.ui[class*="bottom attached"].label { - top: auto; - bottom: 0; - border-radius: 0 0 0.21428571rem 0.21428571rem; -} - -.ui[class*="top left attached"].label { - width: auto; - margin-top: 0; - border-radius: 0.21428571rem 0 0.28571429rem 0; -} - -.ui[class*="top right attached"].label { - width: auto; - left: auto; - right: 0; - border-radius: 0 0.21428571rem 0 0.28571429rem; -} - -.ui[class*="bottom left attached"].label { - width: auto; - top: auto; - bottom: 0; - border-radius: 0 0.28571429rem 0 0.21428571rem; -} - -.ui[class*="bottom right attached"].label { - top: auto; - bottom: 0; - left: auto; - right: 0; - width: auto; - border-radius: 0.28571429rem 0 0.21428571rem 0; -} - -/******************************* - States -*******************************/ - -/*------------------- - Disabled ---------------------*/ - -.ui.label.disabled { - opacity: 0.5; -} - -/*------------------- - Hover ---------------------*/ - -.ui.labels a.label:hover, -a.ui.label:hover { - background-color: #E0E0E0; - border-color: #E0E0E0; - background-image: none; - color: rgba(0, 0, 0, 0.8); -} - -.ui.labels a.label:hover:before, -a.ui.label:hover:before { - color: rgba(0, 0, 0, 0.8); -} - -/*------------------- - Active ---------------------*/ - -.ui.active.label { - background-color: #D0D0D0; - border-color: #D0D0D0; - background-image: none; - color: rgba(0, 0, 0, 0.95); -} - -.ui.active.label:before { - background-color: #D0D0D0; - background-image: none; - color: rgba(0, 0, 0, 0.95); -} - -/*------------------- - Active Hover ---------------------*/ - -.ui.labels a.active.label:hover, -a.ui.active.label:hover { - background-color: #C8C8C8; - border-color: #C8C8C8; - background-image: none; - color: rgba(0, 0, 0, 0.95); -} - -.ui.labels a.active.label:hover:before, -a.ui.active.label:hover:before { - background-color: #C8C8C8; - background-image: none; - color: rgba(0, 0, 0, 0.95); -} - -/*------------------- - Visible ---------------------*/ - -.ui.labels.visible .label, -.ui.label.visible:not(.dropdown) { - display: inline-block !important; -} - -/*------------------- - Hidden ---------------------*/ - -.ui.labels.hidden .label, -.ui.label.hidden { - display: none !important; -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Basic - --------------------*/ - -.ui.basic.labels .label, -.ui.basic.label { - background: none #FFFFFF; - border: 1px solid rgba(34, 36, 38, 0.15); - color: rgba(0, 0, 0, 0.87); - box-shadow: none; - padding-top: calc(0.5833em - 1px); - padding-bottom: calc(0.5833em - 1px); - padding-right: calc(0.833em - 1px); -} - -.ui.basic.labels:not(.tag):not(.image):not(.ribbon) .label, -.ui.basic.label:not(.tag):not(.image):not(.ribbon) { - padding-left: calc(0.833em - 1px); -} - -/* Link */ - -.ui.basic.labels a.label:hover, -a.ui.basic.label:hover { - text-decoration: none; - background: none #FFFFFF; - color: #1e70bf; - box-shadow: none; -} - -/* Pointing */ - -.ui.basic.pointing.label:before { - border-color: inherit; -} - -/*------------------- - Fluid - --------------------*/ - -.ui.label.fluid, -.ui.fluid.labels > .label { - width: 100%; - box-sizing: border-box; -} - -/*------------------- - Colors ---------------------*/ - -.ui.primary.labels .label, -.ui.ui.ui.primary.label { - background-color: #2185D0; - border-color: #2185D0; - color: rgba(255, 255, 255, 0.9); -} - -/* Link */ - -.ui.primary.labels a.label:hover, -a.ui.ui.ui.primary.label:hover { - background-color: #1678c2; - border-color: #1678c2; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .primary.label, -.ui.ui.ui.basic.primary.label { - background: none #FFFFFF; - border-color: #2185D0; - color: #2185D0; -} - -.ui.basic.labels a.primary.label:hover, -a.ui.ui.ui.basic.primary.label:hover { - background: none #FFFFFF; - border-color: #1678c2; - color: #1678c2; -} - -.ui.secondary.labels .label, -.ui.ui.ui.secondary.label { - background-color: #1B1C1D; - border-color: #1B1C1D; - color: rgba(255, 255, 255, 0.9); -} - -/* Link */ - -.ui.secondary.labels a.label:hover, -a.ui.ui.ui.secondary.label:hover { - background-color: #27292a; - border-color: #27292a; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .secondary.label, -.ui.ui.ui.basic.secondary.label { - background: none #FFFFFF; - border-color: #1B1C1D; - color: #1B1C1D; -} - -.ui.basic.labels a.secondary.label:hover, -a.ui.ui.ui.basic.secondary.label:hover { - background: none #FFFFFF; - border-color: #27292a; - color: #27292a; -} - -.ui.red.labels .label, -.ui.ui.ui.red.label { - background-color: #DB2828; - border-color: #DB2828; - color: #FFFFFF; -} - -/* Link */ - -.ui.red.labels a.label:hover, -a.ui.ui.ui.red.label:hover { - background-color: #d01919; - border-color: #d01919; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .red.label, -.ui.ui.ui.basic.red.label { - background: none #FFFFFF; - border-color: #DB2828; - color: #DB2828; -} - -.ui.basic.labels a.red.label:hover, -a.ui.ui.ui.basic.red.label:hover { - background: none #FFFFFF; - border-color: #d01919; - color: #d01919; -} - -.ui.orange.labels .label, -.ui.ui.ui.orange.label { - background-color: #F2711C; - border-color: #F2711C; - color: #FFFFFF; -} - -/* Link */ - -.ui.orange.labels a.label:hover, -a.ui.ui.ui.orange.label:hover { - background-color: #f26202; - border-color: #f26202; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .orange.label, -.ui.ui.ui.basic.orange.label { - background: none #FFFFFF; - border-color: #F2711C; - color: #F2711C; -} - -.ui.basic.labels a.orange.label:hover, -a.ui.ui.ui.basic.orange.label:hover { - background: none #FFFFFF; - border-color: #f26202; - color: #f26202; -} - -.ui.yellow.labels .label, -.ui.ui.ui.yellow.label { - background-color: #FBBD08; - border-color: #FBBD08; - color: #FFFFFF; -} - -/* Link */ - -.ui.yellow.labels a.label:hover, -a.ui.ui.ui.yellow.label:hover { - background-color: #eaae00; - border-color: #eaae00; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .yellow.label, -.ui.ui.ui.basic.yellow.label { - background: none #FFFFFF; - border-color: #FBBD08; - color: #FBBD08; -} - -.ui.basic.labels a.yellow.label:hover, -a.ui.ui.ui.basic.yellow.label:hover { - background: none #FFFFFF; - border-color: #eaae00; - color: #eaae00; -} - -.ui.olive.labels .label, -.ui.ui.ui.olive.label { - background-color: #B5CC18; - border-color: #B5CC18; - color: #FFFFFF; -} - -/* Link */ - -.ui.olive.labels a.label:hover, -a.ui.ui.ui.olive.label:hover { - background-color: #a7bd0d; - border-color: #a7bd0d; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .olive.label, -.ui.ui.ui.basic.olive.label { - background: none #FFFFFF; - border-color: #B5CC18; - color: #B5CC18; -} - -.ui.basic.labels a.olive.label:hover, -a.ui.ui.ui.basic.olive.label:hover { - background: none #FFFFFF; - border-color: #a7bd0d; - color: #a7bd0d; -} - -.ui.green.labels .label, -.ui.ui.ui.green.label { - background-color: #21BA45; - border-color: #21BA45; - color: #FFFFFF; -} - -/* Link */ - -.ui.green.labels a.label:hover, -a.ui.ui.ui.green.label:hover { - background-color: #16ab39; - border-color: #16ab39; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .green.label, -.ui.ui.ui.basic.green.label { - background: none #FFFFFF; - border-color: #21BA45; - color: #21BA45; -} - -.ui.basic.labels a.green.label:hover, -a.ui.ui.ui.basic.green.label:hover { - background: none #FFFFFF; - border-color: #16ab39; - color: #16ab39; -} - -.ui.teal.labels .label, -.ui.ui.ui.teal.label { - background-color: #00B5AD; - border-color: #00B5AD; - color: #FFFFFF; -} - -/* Link */ - -.ui.teal.labels a.label:hover, -a.ui.ui.ui.teal.label:hover { - background-color: #009c95; - border-color: #009c95; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .teal.label, -.ui.ui.ui.basic.teal.label { - background: none #FFFFFF; - border-color: #00B5AD; - color: #00B5AD; -} - -.ui.basic.labels a.teal.label:hover, -a.ui.ui.ui.basic.teal.label:hover { - background: none #FFFFFF; - border-color: #009c95; - color: #009c95; -} - -.ui.blue.labels .label, -.ui.ui.ui.blue.label { - background-color: #2185D0; - border-color: #2185D0; - color: #FFFFFF; -} - -/* Link */ - -.ui.blue.labels a.label:hover, -a.ui.ui.ui.blue.label:hover { - background-color: #1678c2; - border-color: #1678c2; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .blue.label, -.ui.ui.ui.basic.blue.label { - background: none #FFFFFF; - border-color: #2185D0; - color: #2185D0; -} - -.ui.basic.labels a.blue.label:hover, -a.ui.ui.ui.basic.blue.label:hover { - background: none #FFFFFF; - border-color: #1678c2; - color: #1678c2; -} - -.ui.violet.labels .label, -.ui.ui.ui.violet.label { - background-color: #6435C9; - border-color: #6435C9; - color: #FFFFFF; -} - -/* Link */ - -.ui.violet.labels a.label:hover, -a.ui.ui.ui.violet.label:hover { - background-color: #5829bb; - border-color: #5829bb; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .violet.label, -.ui.ui.ui.basic.violet.label { - background: none #FFFFFF; - border-color: #6435C9; - color: #6435C9; -} - -.ui.basic.labels a.violet.label:hover, -a.ui.ui.ui.basic.violet.label:hover { - background: none #FFFFFF; - border-color: #5829bb; - color: #5829bb; -} - -.ui.purple.labels .label, -.ui.ui.ui.purple.label { - background-color: #A333C8; - border-color: #A333C8; - color: #FFFFFF; -} - -/* Link */ - -.ui.purple.labels a.label:hover, -a.ui.ui.ui.purple.label:hover { - background-color: #9627ba; - border-color: #9627ba; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .purple.label, -.ui.ui.ui.basic.purple.label { - background: none #FFFFFF; - border-color: #A333C8; - color: #A333C8; -} - -.ui.basic.labels a.purple.label:hover, -a.ui.ui.ui.basic.purple.label:hover { - background: none #FFFFFF; - border-color: #9627ba; - color: #9627ba; -} - -.ui.pink.labels .label, -.ui.ui.ui.pink.label { - background-color: #E03997; - border-color: #E03997; - color: #FFFFFF; -} - -/* Link */ - -.ui.pink.labels a.label:hover, -a.ui.ui.ui.pink.label:hover { - background-color: #e61a8d; - border-color: #e61a8d; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .pink.label, -.ui.ui.ui.basic.pink.label { - background: none #FFFFFF; - border-color: #E03997; - color: #E03997; -} - -.ui.basic.labels a.pink.label:hover, -a.ui.ui.ui.basic.pink.label:hover { - background: none #FFFFFF; - border-color: #e61a8d; - color: #e61a8d; -} - -.ui.brown.labels .label, -.ui.ui.ui.brown.label { - background-color: #A5673F; - border-color: #A5673F; - color: #FFFFFF; -} - -/* Link */ - -.ui.brown.labels a.label:hover, -a.ui.ui.ui.brown.label:hover { - background-color: #975b33; - border-color: #975b33; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .brown.label, -.ui.ui.ui.basic.brown.label { - background: none #FFFFFF; - border-color: #A5673F; - color: #A5673F; -} - -.ui.basic.labels a.brown.label:hover, -a.ui.ui.ui.basic.brown.label:hover { - background: none #FFFFFF; - border-color: #975b33; - color: #975b33; -} - -.ui.grey.labels .label, -.ui.ui.ui.grey.label { - background-color: #767676; - border-color: #767676; - color: #FFFFFF; -} - -/* Link */ - -.ui.grey.labels a.label:hover, -a.ui.ui.ui.grey.label:hover { - background-color: #838383; - border-color: #838383; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .grey.label, -.ui.ui.ui.basic.grey.label { - background: none #FFFFFF; - border-color: #767676; - color: #767676; -} - -.ui.basic.labels a.grey.label:hover, -a.ui.ui.ui.basic.grey.label:hover { - background: none #FFFFFF; - border-color: #838383; - color: #838383; -} - -.ui.black.labels .label, -.ui.ui.ui.black.label { - background-color: #1B1C1D; - border-color: #1B1C1D; - color: #FFFFFF; -} - -/* Link */ - -.ui.black.labels a.label:hover, -a.ui.ui.ui.black.label:hover { - background-color: #27292a; - border-color: #27292a; - color: #FFFFFF; -} - -/* Basic */ - -.ui.basic.labels .black.label, -.ui.ui.ui.basic.black.label { - background: none #FFFFFF; - border-color: #1B1C1D; - color: #1B1C1D; -} - -.ui.basic.labels a.black.label:hover, -a.ui.ui.ui.basic.black.label:hover { - background: none #FFFFFF; - border-color: #27292a; - color: #27292a; -} - -/*------------------- - Horizontal ---------------------*/ - -.ui.horizontal.labels .label, -.ui.horizontal.label { - margin: 0 0.5em 0 0; - padding: 0.4em 0.833em; - min-width: 3em; - text-align: center; -} - -/*------------------- - Circular - --------------------*/ - -.ui.circular.labels .label, -.ui.circular.label { - min-width: 2em; - min-height: 2em; - padding: 0.5em !important; - line-height: 1em; - text-align: center; - border-radius: 500rem; -} - -.ui.empty.circular.labels .label, -.ui.empty.circular.label { - min-width: 0; - min-height: 0; - overflow: hidden; - width: 0.5em; - height: 0.5em; - vertical-align: baseline; -} - -/*------------------- - Pointing - --------------------*/ - -.ui.pointing.label { - position: relative; -} - -.ui.attached.pointing.label { - position: absolute; -} - -.ui.pointing.label:before { - background-color: inherit; - background-image: inherit; - border-width: 0; - border-style: solid; - border-color: inherit; -} - -/* Arrow */ - -.ui.pointing.label:before { - position: absolute; - content: ''; - transform: rotate(45deg); - background-image: none; - z-index: 2; - width: 0.6666em; - height: 0.6666em; - transition: none; -} - -/*--- Above ---*/ - -.ui.pointing.label, -.ui[class*="pointing above"].label { - margin-top: 1em; -} - -.ui.pointing.label:before, -.ui[class*="pointing above"].label:before { - border-width: 1px 0 0 1px; - transform: translateX(-50%) translateY(-50%) rotate(45deg); - top: 0; - left: 50%; -} - -/*--- Below ---*/ - -.ui[class*="bottom pointing"].label, -.ui[class*="pointing below"].label { - margin-top: 0; - margin-bottom: 1em; -} - -.ui[class*="bottom pointing"].label:before, -.ui[class*="pointing below"].label:before { - border-width: 0 1px 1px 0; - top: auto; - right: auto; - transform: translateX(-50%) translateY(-50%) rotate(45deg); - top: 100%; - left: 50%; -} - -/*--- Left ---*/ - -.ui[class*="left pointing"].label { - margin-top: 0; - margin-left: 0.6666em; -} - -.ui[class*="left pointing"].label:before { - border-width: 0 0 1px 1px; - transform: translateX(-50%) translateY(-50%) rotate(45deg); - bottom: auto; - right: auto; - top: 50%; - left: 0; -} - -/*--- Right ---*/ - -.ui[class*="right pointing"].label { - margin-top: 0; - margin-right: 0.6666em; -} - -.ui[class*="right pointing"].label:before { - border-width: 1px 1px 0 0; - transform: translateX(50%) translateY(-50%) rotate(45deg); - top: 50%; - right: 0; - bottom: auto; - left: auto; -} - -/* Basic Pointing */ - -/*--- Above ---*/ - -.ui.basic.pointing.label:before, -.ui.basic[class*="pointing above"].label:before { - margin-top: -1px; -} - -/*--- Below ---*/ - -.ui.basic[class*="bottom pointing"].label:before, -.ui.basic[class*="pointing below"].label:before { - bottom: auto; - top: 100%; - margin-top: 1px; -} - -/*--- Left ---*/ - -.ui.basic[class*="left pointing"].label:before { - top: 50%; - left: -1px; -} - -/*--- Right ---*/ - -.ui.basic[class*="right pointing"].label:before { - top: 50%; - right: -1px; -} - -/*------------------ - Floating Label - -------------------*/ - -.ui.floating.label { - position: absolute; - z-index: 100; - top: -1em; - right: 0; - white-space: nowrap; - transform: translateX(50%); -} - -.ui.right.aligned.floating.label { - transform: translateX(1.2em); -} - -.ui.left.floating.label { - left: 0; - right: auto; - transform: translateX(-50%); -} - -.ui.left.aligned.floating.label { - transform: translateX(-1.2em); -} - -.ui.bottom.floating.label { - top: auto; - bottom: -1em; -} - -/*------------------- - Sizes ---------------------*/ - -.ui.labels .label, -.ui.label { - font-size: 0.85714286rem; -} - -.ui.mini.labels .label, -.ui.mini.label { - font-size: 0.64285714rem; -} - -.ui.tiny.labels .label, -.ui.tiny.label { - font-size: 0.71428571rem; -} - -.ui.small.labels .label, -.ui.small.label { - font-size: 0.78571429rem; -} - -.ui.large.labels .label, -.ui.large.label { - font-size: 1rem; -} - -.ui.big.labels .label, -.ui.big.label { - font-size: 1.28571429rem; -} - -.ui.huge.labels .label, -.ui.huge.label { - font-size: 1.42857143rem; -} - -.ui.massive.labels .label, -.ui.massive.label { - font-size: 1.71428571rem; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - List - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - List -*******************************/ - -ul.ui.list, -ol.ui.list, -.ui.list { - list-style-type: none; - margin: 1em 0; - padding: 0 0; -} - -ul.ui.list:first-child, -ol.ui.list:first-child, -.ui.list:first-child { - margin-top: 0; - padding-top: 0; -} - -ul.ui.list:last-child, -ol.ui.list:last-child, -.ui.list:last-child { - margin-bottom: 0; - padding-bottom: 0; -} - -/******************************* - Content -*******************************/ - -/* List Item */ - -ul.ui.list li, -ol.ui.list li, -.ui.list > .item, -.ui.list .list > .item { - display: list-item; - table-layout: fixed; - list-style-type: none; - list-style-position: outside; - padding: 0.21428571em 0; - line-height: 1.14285714em; -} - -ul.ui.list > li:first-child:after, -ol.ui.list > li:first-child:after, -.ui.list > .list > .item:after, -.ui.list > .item:after { - content: ''; - display: block; - height: 0; - clear: both; - visibility: hidden; -} - -ul.ui.list li:first-child, -ol.ui.list li:first-child, -.ui.list .list > .item:first-child, -.ui.list > .item:first-child { - padding-top: 0; -} - -ul.ui.list li:last-child, -ol.ui.list li:last-child, -.ui.list .list > .item:last-child, -.ui.list > .item:last-child { - padding-bottom: 0; -} - -/* Child List */ - -ul.ui.list ul, -ol.ui.list ol, -.ui.list .list:not(.icon) { - clear: both; - margin: 0; - padding: 0.75em 0 0.25em 0.5em; -} - -/* Child Item */ - -ul.ui.list ul li, -ol.ui.list ol li, -.ui.list .list > .item { - padding: 0.14285714em 0; - line-height: inherit; -} - -/* Icon */ - -.ui.list .list > .item > i.icon, -.ui.list > .item > i.icon { - display: table-cell; - min-width: 1.55em; - margin: 0; - padding-top: 0; - transition: color 0.1s ease; -} - -.ui.list .list > .item > i.icon:not(.loading), -.ui.list > .item > i.icon:not(.loading) { - padding-right: 0.28571429em; - vertical-align: top; -} - -.ui.list .list > .item > i.icon:only-child, -.ui.list > .item > i.icon:only-child { - display: inline-block; - min-width: auto; - vertical-align: top; -} - -/* Image */ - -.ui.list .list > .item > .image, -.ui.list > .item > .image { - display: table-cell; - background-color: transparent; - margin: 0; - vertical-align: top; -} - -.ui.list .list > .item > .image:not(:only-child):not(img), -.ui.list > .item > .image:not(:only-child):not(img) { - padding-right: 0.5em; -} - -.ui.list .list > .item > .image img, -.ui.list > .item > .image img { - vertical-align: top; -} - -.ui.list .list > .item > img.image, -.ui.list .list > .item > .image:only-child, -.ui.list > .item > img.image, -.ui.list > .item > .image:only-child { - display: inline-block; -} - -/* Content */ - -.ui.list .list > .item > .content, -.ui.list > .item > .content { - line-height: 1.14285714em; - color: rgba(0, 0, 0, 0.87); -} - -.ui.list .list > .item > .image + .content, -.ui.list .list > .item > i.icon + .content, -.ui.list > .item > .image + .content, -.ui.list > .item > i.icon + .content { - display: table-cell; - width: 100%; - padding: 0 0 0 0.5em; - vertical-align: top; -} - -.ui.list .list > .item > i.loading.icon + .content, -.ui.list > .item > i.loading.icon + .content { - padding-left: calc(0.2857142857142857em + 0.5em); -} - -.ui.list .list > .item > img.image + .content, -.ui.list > .item > img.image + .content { - display: inline-block; - width: auto; -} - -.ui.list .list > .item > .content > .list, -.ui.list > .item > .content > .list { - margin-left: 0; - padding-left: 0; -} - -/* Header */ - -.ui.list .list > .item .header, -.ui.list > .item .header { - display: block; - margin: 0; - font-family: var(--fonts-regular); - font-weight: 500; - color: rgba(0, 0, 0, 0.87); -} - -/* Description */ - -.ui.list .list > .item .description, -.ui.list > .item .description { - display: block; - color: rgba(0, 0, 0, 0.7); -} - -/* Child Link */ - -.ui.list > .item a, -.ui.list .list > .item a { - cursor: pointer; -} - -/* Linking Item */ - -.ui.list .list > a.item, -.ui.list > a.item { - cursor: pointer; - color: #4183C4; -} - -.ui.list .list > a.item:hover, -.ui.list > a.item:hover { - color: #1e70bf; -} - -/* Linked Item Icons */ - -.ui.list .list > a.item > i.icons, -.ui.list > a.item > i.icons, -.ui.list .list > a.item > i.icon, -.ui.list > a.item > i.icon { - color: rgba(0, 0, 0, 0.4); -} - -/* Header Link */ - -.ui.list .list > .item a.header, -.ui.list > .item a.header { - cursor: pointer; - color: #4183C4 !important; -} - -.ui.list .list > .item > a.header:hover, -.ui.list > .item > a.header:hover { - color: #1e70bf !important; -} - -/* Floated Content */ - -.ui[class*="left floated"].list { - float: left; -} - -.ui[class*="right floated"].list { - float: right; -} - -.ui.list .list > .item [class*="left floated"], -.ui.list > .item [class*="left floated"] { - float: left; - margin: 0 1em 0 0; -} - -.ui.list .list > .item [class*="right floated"], -.ui.list > .item [class*="right floated"] { - float: right; - margin: 0 0 0 1em; -} - -/******************************* - Coupling -*******************************/ - -.ui.menu .ui.list > .item, -.ui.menu .ui.list .list > .item { - display: list-item; - table-layout: fixed; - background-color: transparent; - list-style-type: none; - list-style-position: outside; - padding: 0.21428571em 0; - line-height: 1.14285714em; -} - -.ui.menu .ui.list .list > .item:before, -.ui.menu .ui.list > .item:before { - border: none; - background: none; -} - -.ui.menu .ui.list .list > .item:first-child, -.ui.menu .ui.list > .item:first-child { - padding-top: 0; -} - -.ui.menu .ui.list .list > .item:last-child, -.ui.menu .ui.list > .item:last-child { - padding-bottom: 0; -} - -/******************************* - Types -*******************************/ - -/*------------------- - Horizontal - --------------------*/ - -.ui.horizontal.list { - display: inline-block; - font-size: 0; -} - -.ui.horizontal.list > .item { - display: inline-block; - margin-right: 1em; - font-size: 1rem; -} - -.ui.horizontal.list:not(.celled) > .item:last-child { - margin-right: 0; - padding-right: 0; -} - -.ui.horizontal.list .list:not(.icon) { - padding-left: 0; - padding-bottom: 0; -} - -.ui.horizontal.list > .item > .image, -.ui.horizontal.list .list > .item > .image, -.ui.horizontal.list > .item > i.icon, -.ui.horizontal.list .list > .item > i.icon, -.ui.horizontal.list > .item > .content, -.ui.horizontal.list .list > .item > .content { - vertical-align: middle; -} - -/* Padding on all elements */ - -.ui.horizontal.list > .item:first-child, -.ui.horizontal.list > .item:last-child { - padding-top: 0.21428571em; - padding-bottom: 0.21428571em; -} - -/* Horizontal List */ - -.ui.horizontal.list > .item > i.icon, -.ui.horizontal.list .item > i.icons > i.icon { - margin: 0; - padding: 0 0.25em 0 0; -} - -.ui.horizontal.list > .item > .image + .content, -.ui.horizontal.list > .item > i.icon, -.ui.horizontal.list > .item > i.icon + .content { - float: none; - display: inline-block; - width: auto; -} - -.ui.horizontal.list > .item > .image { - display: inline-block; -} - -/******************************* - States -*******************************/ - -/*------------------- - Disabled - --------------------*/ - -.ui.list .list > .disabled.item, -.ui.list > .disabled.item { - pointer-events: none; - color: rgba(40, 40, 40, 0.3) !important; -} - -/*------------------- - Hover ---------------------*/ - -.ui.list .list > a.item:hover > .icons, -.ui.list > a.item:hover > .icons, -.ui.list .list > a.item:hover > i.icon, -.ui.list > a.item:hover > i.icon { - color: rgba(0, 0, 0, 0.87); -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Aligned - --------------------*/ - -.ui.list[class*="top aligned"] .image, -.ui.list[class*="top aligned"] .content, -.ui.list [class*="top aligned"] { - vertical-align: top !important; -} - -.ui.list[class*="middle aligned"] .image, -.ui.list[class*="middle aligned"] .content, -.ui.list [class*="middle aligned"] { - vertical-align: middle !important; -} - -.ui.list[class*="bottom aligned"] .image, -.ui.list[class*="bottom aligned"] .content, -.ui.list [class*="bottom aligned"] { - vertical-align: bottom !important; -} - -/*------------------- - Link - --------------------*/ - -.ui.link.list .item, -.ui.link.list a.item, -.ui.link.list .item a:not(.ui) { - color: rgba(0, 0, 0, 0.4); - transition: 0.1s color ease; -} - -.ui.link.list.list a.item:hover, -.ui.link.list.list .item a:not(.ui):hover { - color: rgba(0, 0, 0, 0.8); -} - -.ui.link.list.list a.item:active, -.ui.link.list.list .item a:not(.ui):active { - color: rgba(0, 0, 0, 0.9); -} - -.ui.link.list.list .active.item, -.ui.link.list.list .active.item a:not(.ui) { - color: rgba(0, 0, 0, 0.95); -} - -/*------------------- - Selection - --------------------*/ - -.ui.selection.list .list > .item, -.ui.selection.list > .item { - cursor: pointer; - background: transparent; - padding: 0.5em 0.5em; - margin: 0; - color: rgba(0, 0, 0, 0.4); - border-radius: 0.5em; - transition: 0.1s color ease, 0.1s padding-left ease, 0.1s background-color ease; -} - -.ui.selection.list .list > .item:last-child, -.ui.selection.list > .item:last-child { - margin-bottom: 0; -} - -.ui.selection.list .list > .item:hover, -.ui.selection.list > .item:hover { - background: rgba(0, 0, 0, 0.03); - color: rgba(0, 0, 0, 0.8); -} - -.ui.selection.list .list > .item:active, -.ui.selection.list > .item:active { - background: rgba(0, 0, 0, 0.05); - color: rgba(0, 0, 0, 0.9); -} - -.ui.selection.list .list > .item.active, -.ui.selection.list > .item.active { - background: rgba(0, 0, 0, 0.05); - color: rgba(0, 0, 0, 0.95); -} - -/* Celled / Divided Selection List */ - -.ui.celled.selection.list .list > .item, -.ui.divided.selection.list .list > .item, -.ui.celled.selection.list > .item, -.ui.divided.selection.list > .item { - border-radius: 0; -} - -/*------------------- - Animated - --------------------*/ - -.ui.animated.list > .item { - transition: 0.25s color ease 0.1s, 0.25s padding-left ease 0.1s, 0.25s background-color ease 0.1s; -} - -.ui.animated.list:not(.horizontal) > .item:hover { - padding-left: 1em; -} - -/*------------------- - Fitted - --------------------*/ - -.ui.fitted.list:not(.selection) .list > .item, -.ui.fitted.list:not(.selection) > .item { - padding-left: 0; - padding-right: 0; -} - -.ui.fitted.selection.list .list > .item, -.ui.fitted.selection.list > .item { - margin-left: -0.5em; - margin-right: -0.5em; -} - -/*------------------- - Bulleted - --------------------*/ - -ul.ui.list, -.ui.bulleted.list { - margin-left: 1.25rem; -} - -ul.ui.list li, -.ui.bulleted.list .list > .item, -.ui.bulleted.list > .item { - position: relative; -} - -ul.ui.list li:before, -.ui.bulleted.list .list > .item:before, -.ui.bulleted.list > .item:before { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - pointer-events: none; - position: absolute; - top: auto; - left: auto; - font-weight: normal; - margin-left: -1.25rem; - content: '\2022'; - opacity: 1; - color: inherit; - vertical-align: top; -} - -ul.ui.list li:before, -.ui.bulleted.list .list > a.item:before, -.ui.bulleted.list > a.item:before { - color: rgba(0, 0, 0, 0.87); -} - -ul.ui.list ul, -.ui.bulleted.list .list:not(.icon) { - padding-left: 1.25rem; -} - -/* Horizontal Bulleted */ - -ul.ui.horizontal.bulleted.list, -.ui.horizontal.bulleted.list { - margin-left: 0; -} - -ul.ui.horizontal.bulleted.list li, -.ui.horizontal.bulleted.list > .item { - margin-left: 1.75rem; -} - -ul.ui.horizontal.bulleted.list li:first-child, -.ui.horizontal.bulleted.list > .item:first-child { - margin-left: 0; -} - -ul.ui.horizontal.bulleted.list li::before, -.ui.horizontal.bulleted.list > .item::before { - color: rgba(0, 0, 0, 0.87); -} - -ul.ui.horizontal.bulleted.list li:first-child::before, -.ui.horizontal.bulleted.list > .item:first-child::before { - display: none; -} - -/*------------------- - Ordered - --------------------*/ - -ol.ui.list, -.ui.ordered.list, -.ui.ordered.list .list:not(.icon), -ol.ui.list ol { - counter-reset: ordered; - margin-left: 1.25rem; - list-style-type: none; -} - -ol.ui.list li, -.ui.ordered.list .list > .item, -.ui.ordered.list > .item { - list-style-type: none; - position: relative; -} - -ol.ui.list li:before, -.ui.ordered.list .list > .item:before, -.ui.ordered.list > .item:before { - position: absolute; - top: auto; - left: auto; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - pointer-events: none; - margin-left: -1.25rem; - counter-increment: ordered; - content: counters(ordered, ".") " "; - text-align: right; - color: rgba(0, 0, 0, 0.87); - vertical-align: middle; - opacity: 0.8; -} - -/* Value */ - -.ui.ordered.list .list > .item[data-value]:before, -.ui.ordered.list > .item[data-value]:before { - content: attr(data-value); -} - -ol.ui.list li[value]:before { - content: attr(value); -} - -/* Child Lists */ - -ol.ui.list ol, -.ui.ordered.list .list:not(.icon) { - margin-left: 1em; -} - -ol.ui.list ol li:before, -.ui.ordered.list .list > .item:before { - margin-left: -2em; -} - -/* Horizontal Ordered */ - -ol.ui.horizontal.list, -.ui.ordered.horizontal.list { - margin-left: 0; -} - -ol.ui.horizontal.list li:before, -.ui.ordered.horizontal.list .list > .item:before, -.ui.ordered.horizontal.list > .item:before { - position: static; - margin: 0 0.5em 0 0; -} - -/* Suffixed Ordered */ - -ol.ui.suffixed.list li:before, -.ui.suffixed.ordered.list .list > .item:before, -.ui.suffixed.ordered.list > .item:before { - content: counters(ordered, ".") "."; -} - -/*------------------- - Divided - --------------------*/ - -.ui.divided.list > .item { - border-top: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.divided.list .list > .item { - border-top: none; -} - -.ui.divided.list .item .list > .item { - border-top: none; -} - -.ui.divided.list .list > .item:first-child, -.ui.divided.list > .item:first-child { - border-top: none; -} - -/* Sub Menu */ - -.ui.divided.list:not(.horizontal) .list > .item:first-child { - border-top-width: 1px; -} - -/* Divided bulleted */ - -.ui.divided.bulleted.list:not(.horizontal), -.ui.divided.bulleted.list .list:not(.icon) { - margin-left: 0; - padding-left: 0; -} - -.ui.divided.bulleted.list > .item:not(.horizontal) { - padding-left: 1.25rem; -} - -/* Divided Ordered */ - -.ui.divided.ordered.list { - margin-left: 0; -} - -.ui.divided.ordered.list .list > .item, -.ui.divided.ordered.list > .item { - padding-left: 1.25rem; -} - -.ui.divided.ordered.list .item .list:not(.icon) { - margin-left: 0; - margin-right: 0; - padding-bottom: 0.21428571em; -} - -.ui.divided.ordered.list .item .list > .item { - padding-left: 1em; -} - -/* Divided Selection */ - -.ui.divided.selection.list .list > .item, -.ui.divided.selection.list > .item { - margin: 0; - border-radius: 0; -} - -/* Divided horizontal */ - -.ui.divided.horizontal.list { - margin-left: 0; -} - -.ui.divided.horizontal.list > .item { - padding-left: 0.5em; -} - -.ui.divided.horizontal.list > .item:not(:last-child) { - padding-right: 0.5em; -} - -.ui.divided.horizontal.list > .item { - border-top: none; - border-right: 1px solid rgba(34, 36, 38, 0.15); - margin: 0; - line-height: 0.6; -} - -.ui.horizontal.divided.list > .item:last-child { - border-right: none; -} - -/*------------------- - Celled - --------------------*/ - -.ui.celled.list > .item, -.ui.celled.list > .list { - border-top: 1px solid rgba(34, 36, 38, 0.15); - padding-left: 0.5em; - padding-right: 0.5em; -} - -.ui.celled.list > .item:last-child { - border-bottom: 1px solid rgba(34, 36, 38, 0.15); -} - -/* Padding on all elements */ - -.ui.celled.list > .item:first-child, -.ui.celled.list > .item:last-child { - padding-top: 0.21428571em; - padding-bottom: 0.21428571em; -} - -/* Sub Menu */ - -.ui.celled.list .item .list > .item { - border-width: 0; -} - -.ui.celled.list .list > .item:first-child { - border-top-width: 0; -} - -/* Celled Bulleted */ - -.ui.celled.bulleted.list { - margin-left: 0; -} - -.ui.celled.bulleted.list .list > .item, -.ui.celled.bulleted.list > .item { - padding-left: 1.25rem; -} - -.ui.celled.bulleted.list .item .list:not(.icon) { - margin-left: -1.25rem; - margin-right: -1.25rem; - padding-bottom: 0.21428571em; -} - -/* Celled Ordered */ - -.ui.celled.ordered.list { - margin-left: 0; -} - -.ui.celled.ordered.list .list > .item, -.ui.celled.ordered.list > .item { - padding-left: 1.25rem; -} - -.ui.celled.ordered.list .item .list:not(.icon) { - margin-left: 0; - margin-right: 0; - padding-bottom: 0.21428571em; -} - -.ui.celled.ordered.list .list > .item { - padding-left: 1em; -} - -/* Celled Horizontal */ - -.ui.horizontal.celled.list { - margin-left: 0; -} - -.ui.horizontal.celled.list .list > .item, -.ui.horizontal.celled.list > .item { - border-top: none; - border-left: 1px solid rgba(34, 36, 38, 0.15); - margin: 0; - padding-left: 0.5em; - padding-right: 0.5em; - line-height: 0.6; -} - -.ui.horizontal.celled.list .list > .item:last-child, -.ui.horizontal.celled.list > .item:last-child { - border-bottom: none; - border-right: 1px solid rgba(34, 36, 38, 0.15); -} - -/*------------------- - Relaxed - --------------------*/ - -.ui.relaxed.list:not(.horizontal) > .item:not(:first-child) { - padding-top: 0.42857143em; -} - -.ui.relaxed.list:not(.horizontal) > .item:not(:last-child) { - padding-bottom: 0.42857143em; -} - -.ui.horizontal.relaxed.list .list > .item:not(:first-child), -.ui.horizontal.relaxed.list > .item:not(:first-child) { - padding-left: 1rem; -} - -.ui.horizontal.relaxed.list .list > .item:not(:last-child), -.ui.horizontal.relaxed.list > .item:not(:last-child) { - padding-right: 1rem; -} - -/* Very Relaxed */ - -.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:first-child) { - padding-top: 0.85714286em; -} - -.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:last-child) { - padding-bottom: 0.85714286em; -} - -.ui.horizontal[class*="very relaxed"].list .list > .item:not(:first-child), -.ui.horizontal[class*="very relaxed"].list > .item:not(:first-child) { - padding-left: 1.5rem; -} - -.ui.horizontal[class*="very relaxed"].list .list > .item:not(:last-child), -.ui.horizontal[class*="very relaxed"].list > .item:not(:last-child) { - padding-right: 1.5rem; -} - -/*------------------- - Sizes ---------------------*/ - -.ui.list { - font-size: 1em; -} - -.ui.mini.list { - font-size: 0.78571429em; -} - -.ui.mini.horizontal.list .list > .item, -.ui.mini.horizontal.list > .item { - font-size: 0.78571429rem; -} - -.ui.tiny.list { - font-size: 0.85714286em; -} - -.ui.tiny.horizontal.list .list > .item, -.ui.tiny.horizontal.list > .item { - font-size: 0.85714286rem; -} - -.ui.small.list { - font-size: 0.92857143em; -} - -.ui.small.horizontal.list .list > .item, -.ui.small.horizontal.list > .item { - font-size: 0.92857143rem; -} - -.ui.large.list { - font-size: 1.14285714em; -} - -.ui.large.horizontal.list .list > .item, -.ui.large.horizontal.list > .item { - font-size: 1.14285714rem; -} - -.ui.big.list { - font-size: 1.28571429em; -} - -.ui.big.horizontal.list .list > .item, -.ui.big.horizontal.list > .item { - font-size: 1.28571429rem; -} - -.ui.huge.list { - font-size: 1.42857143em; -} - -.ui.huge.horizontal.list .list > .item, -.ui.huge.horizontal.list > .item { - font-size: 1.42857143rem; -} - -.ui.massive.list { - font-size: 1.71428571em; -} - -.ui.massive.horizontal.list .list > .item, -.ui.massive.horizontal.list > .item { - font-size: 1.71428571rem; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - User Variable Overrides -*******************************/ /* * # Fomantic - Menu * http://github.com/fomantic/Fomantic-UI/ @@ -13306,7 +6764,6 @@ ol.ui.suffixed.list li:before, left: 100%; /* IE needs 0, all others support max-content to show dropdown icon inline, so keep both settings! */ min-width: 0; - min-width: -webkit-max-content; min-width: -moz-max-content; min-width: max-content; margin: 0 0 0 0; @@ -15030,690 +8487,6 @@ Floated Menu / Item /******************************* Site Overrides *******************************/ -/*! - * # Fomantic-UI - Message - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Message -*******************************/ - -.ui.message { - position: relative; - min-height: 1em; - margin: 1em 0; - background: #F8F8F9; - padding: 1em 1.5em; - line-height: 1.4285em; - color: rgba(0, 0, 0, 0.87); - transition: opacity 0.1s ease, color 0.1s ease, background 0.1s ease, box-shadow 0.1s ease; - border-radius: 0.28571429rem; - box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.message:first-child { - margin-top: 0; -} - -.ui.message:last-child { - margin-bottom: 0; -} - -/*-------------- - Content ----------------*/ - -/* Header */ - -.ui.message .header { - display: block; - font-family: var(--fonts-regular); - font-weight: 500; - margin: -0.14285714em 0 0 0; -} - -/* Default font size */ - -.ui.message .header:not(.ui) { - font-size: 1.14285714em; -} - -/* Paragraph */ - -.ui.message p { - opacity: 0.85; - margin: 0.75em 0; -} - -.ui.message p:first-child { - margin-top: 0; -} - -.ui.message p:last-child { - margin-bottom: 0; -} - -.ui.message .header + p { - margin-top: 0.25em; -} - -/* List */ - -.ui.message .list:not(.ui) { - text-align: left; - padding: 0; - opacity: 0.85; - list-style-position: inside; - margin: 0.5em 0 0; -} - -.ui.message .list:not(.ui):first-child { - margin-top: 0; -} - -.ui.message .list:not(.ui):last-child { - margin-bottom: 0; -} - -.ui.message .list:not(.ui) li { - position: relative; - list-style-type: none; - margin: 0 0 0.3em 1em; - padding: 0; -} - -.ui.message .list:not(.ui) li:before { - position: absolute; - content: '•'; - left: -1em; - height: 100%; - vertical-align: baseline; -} - -.ui.message .list:not(.ui) li:last-child { - margin-bottom: 0; -} - -/* Icon */ - -.ui.message > i.icon { - margin-right: 0.6em; -} - -/* Close Icon */ - -.ui.message > .close.icon { - cursor: pointer; - position: absolute; - margin: 0; - top: 0.78575em; - right: 0.5em; - opacity: 0.7; - transition: opacity 0.1s ease; -} - -.ui.message > .close.icon:hover { - opacity: 1; -} - -/* First / Last Element */ - -.ui.message > :first-child { - margin-top: 0; -} - -.ui.message > :last-child { - margin-bottom: 0; -} - -/******************************* - Coupling -*******************************/ - -.ui.dropdown .menu > .message { - margin: 0 -1px; -} - -/******************************* - States -*******************************/ - -/*-------------- - Visible ----------------*/ - -.ui.visible.visible.visible.visible.message { - display: block; -} - -.ui.icon.visible.visible.visible.visible.message { - display: flex; -} - -/*-------------- - Hidden ----------------*/ - -.ui.hidden.hidden.hidden.hidden.message { - display: none; -} - -/******************************* - Variations -*******************************/ - -/*-------------- - Compact - ---------------*/ - -.ui.compact.message { - display: inline-block; -} - -.ui.compact.icon.message { - display: inline-flex; - width: auto; -} - -/*-------------- - Attached - ---------------*/ - -.ui.attached.message { - margin-bottom: -1px; - border-radius: 0.28571429rem 0.28571429rem 0 0; - box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset; - margin-left: -1px; - margin-right: -1px; -} - -.ui.attached + .ui.attached.message:not(.top):not(.bottom) { - margin-top: -1px; - border-radius: 0; -} - -.ui.bottom.attached.message { - margin-top: -1px; - border-radius: 0 0 0.28571429rem 0.28571429rem; - box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset, 0 1px 2px 0 rgba(34, 36, 38, 0.15); -} - -.ui.bottom.attached.message:not(:last-child) { - margin-bottom: 1em; -} - -.ui.attached.icon.message { - width: auto; -} - -/*-------------- - Icon - ---------------*/ - -.ui.icon.message { - display: flex; - width: 100%; - align-items: center; -} - -.ui.icon.message > i.icon:not(.close) { - display: block; - flex: 0 0 auto; - width: auto; - line-height: 1; - vertical-align: middle; - font-size: 3em; - opacity: 0.8; -} - -.ui.icon.message > .content { - display: block; - flex: 1 1 auto; - vertical-align: middle; -} - -.ui.icon.message > i.icon:not(.close) + .content { - padding-left: 0; -} - -.ui.icon.message > i.circular.icon { - width: 1em; -} - -/*-------------- - Floating - ---------------*/ - -.ui.floating.message { - box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.22) inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -/*-------------- - Colors ----------------*/ - -/*-------------- - Types ----------------*/ - -.ui.positive.message { - background-color: #FCFFF5; - color: #2C662D; -} - -.ui.positive.message, -.ui.attached.positive.message { - box-shadow: 0 0 0 1px #A3C293 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.positive.message { - box-shadow: 0 0 0 1px #A3C293 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.positive.message .header { - color: #1A531B; -} - -.ui.negative.message { - background-color: #FFF6F6; - color: #9F3A38; -} - -.ui.negative.message, -.ui.attached.negative.message { - box-shadow: 0 0 0 1px #E0B4B4 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.negative.message { - box-shadow: 0 0 0 1px #E0B4B4 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.negative.message .header { - color: #912D2B; -} - -.ui.info.message { - background-color: #F8FFFF; - color: #276F86; -} - -.ui.info.message, -.ui.attached.info.message { - box-shadow: 0 0 0 1px #A9D5DE inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.info.message { - box-shadow: 0 0 0 1px #A9D5DE inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.info.message .header { - color: #0E566C; -} - -.ui.warning.message { - background-color: #FFFAF3; - color: #573A08; -} - -.ui.warning.message, -.ui.attached.warning.message { - box-shadow: 0 0 0 1px #C9BA9B inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.warning.message { - box-shadow: 0 0 0 1px #C9BA9B inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.warning.message .header { - color: #794B02; -} - -.ui.error.message { - background-color: #FFF6F6; - color: #9F3A38; -} - -.ui.error.message, -.ui.attached.error.message { - box-shadow: 0 0 0 1px #E0B4B4 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.error.message { - box-shadow: 0 0 0 1px #E0B4B4 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.error.message .header { - color: #912D2B; -} - -.ui.success.message { - background-color: #FCFFF5; - color: #2C662D; -} - -.ui.success.message, -.ui.attached.success.message { - box-shadow: 0 0 0 1px #A3C293 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.success.message { - box-shadow: 0 0 0 1px #A3C293 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.success.message .header { - color: #1A531B; -} - -.ui.primary.message { - background-color: #DFF0FF; - color: rgba(255, 255, 255, 0.9); -} - -.ui.primary.message, -.ui.attached.primary.message { - box-shadow: 0 0 0 1px #2185D0 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.primary.message { - box-shadow: 0 0 0 1px #2185D0 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.primary.message .header { - color: rgba(242, 242, 242, 0.9); -} - -.ui.secondary.message { - background-color: #F4F4F4; - color: rgba(255, 255, 255, 0.9); -} - -.ui.secondary.message, -.ui.attached.secondary.message { - box-shadow: 0 0 0 1px #1B1C1D inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.secondary.message { - box-shadow: 0 0 0 1px #1B1C1D inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.secondary.message .header { - color: rgba(242, 242, 242, 0.9); -} - -.ui.red.message { - background-color: #FFE8E6; - color: #DB2828; -} - -.ui.red.message, -.ui.attached.red.message { - box-shadow: 0 0 0 1px #DB2828 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.red.message { - box-shadow: 0 0 0 1px #DB2828 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.red.message .header { - color: #c82121; -} - -.ui.orange.message { - background-color: #FFEDDE; - color: #F2711C; -} - -.ui.orange.message, -.ui.attached.orange.message { - box-shadow: 0 0 0 1px #F2711C inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.orange.message { - box-shadow: 0 0 0 1px #F2711C inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.orange.message .header { - color: #e7640d; -} - -.ui.yellow.message { - background-color: #FFF8DB; - color: #B58105; -} - -.ui.yellow.message, -.ui.attached.yellow.message { - box-shadow: 0 0 0 1px #B58105 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.yellow.message { - box-shadow: 0 0 0 1px #B58105 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.yellow.message .header { - color: #9c6f04; -} - -.ui.olive.message { - background-color: #FBFDEF; - color: #8ABC1E; -} - -.ui.olive.message, -.ui.attached.olive.message { - box-shadow: 0 0 0 1px #8ABC1E inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.olive.message { - box-shadow: 0 0 0 1px #8ABC1E inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.olive.message .header { - color: #7aa61a; -} - -.ui.green.message { - background-color: #E5F9E7; - color: #1EBC30; -} - -.ui.green.message, -.ui.attached.green.message { - box-shadow: 0 0 0 1px #1EBC30 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.green.message { - box-shadow: 0 0 0 1px #1EBC30 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.green.message .header { - color: #1aa62a; -} - -.ui.teal.message { - background-color: #E1F7F7; - color: #10A3A3; -} - -.ui.teal.message, -.ui.attached.teal.message { - box-shadow: 0 0 0 1px #10A3A3 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.teal.message { - box-shadow: 0 0 0 1px #10A3A3 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.teal.message .header { - color: #0e8c8c; -} - -.ui.blue.message { - background-color: #DFF0FF; - color: #2185D0; -} - -.ui.blue.message, -.ui.attached.blue.message { - box-shadow: 0 0 0 1px #2185D0 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.blue.message { - box-shadow: 0 0 0 1px #2185D0 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.blue.message .header { - color: #1e77ba; -} - -.ui.violet.message { - background-color: #EAE7FF; - color: #6435C9; -} - -.ui.violet.message, -.ui.attached.violet.message { - box-shadow: 0 0 0 1px #6435C9 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.violet.message { - box-shadow: 0 0 0 1px #6435C9 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.violet.message .header { - color: #5a30b5; -} - -.ui.purple.message { - background-color: #F6E7FF; - color: #A333C8; -} - -.ui.purple.message, -.ui.attached.purple.message { - box-shadow: 0 0 0 1px #A333C8 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.purple.message { - box-shadow: 0 0 0 1px #A333C8 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.purple.message .header { - color: #922eb4; -} - -.ui.pink.message { - background-color: #FFE3FB; - color: #E03997; -} - -.ui.pink.message, -.ui.attached.pink.message { - box-shadow: 0 0 0 1px #E03997 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.pink.message { - box-shadow: 0 0 0 1px #E03997 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.pink.message .header { - color: #dd238b; -} - -.ui.brown.message { - background-color: #F1E2D3; - color: #A5673F; -} - -.ui.brown.message, -.ui.attached.brown.message { - box-shadow: 0 0 0 1px #A5673F inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.brown.message { - box-shadow: 0 0 0 1px #A5673F inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.brown.message .header { - color: #935b38; -} - -.ui.grey.message { - background-color: #F4F4F4; - color: #767676; -} - -.ui.grey.message, -.ui.attached.grey.message { - box-shadow: 0 0 0 1px #767676 inset, 0 0 0 0 rgba(0, 0, 0, 0); -} - -.ui.floating.grey.message { - box-shadow: 0 0 0 1px #767676 inset, 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -.ui.grey.message .header { - color: #696969; -} - -.ui.black.message { - background-color: #1B1C1D; - color: rgba(255, 255, 255, 0.9); -} - -.ui.black.message .header { - color: rgba(255, 255, 255, 0.9); -} - -/*-------------- - Sizes ----------------*/ - -.ui.message { - font-size: 1em; -} - -.ui.mini.message { - font-size: 0.78571429em; -} - -.ui.tiny.message { - font-size: 0.85714286em; -} - -.ui.small.message { - font-size: 0.92857143em; -} - -.ui.large.message { - font-size: 1.14285714em; -} - -.ui.big.message { - font-size: 1.28571429em; -} - -.ui.huge.message { - font-size: 1.42857143em; -} - -.ui.massive.message { - font-size: 1.71428571em; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - Site Overrides -*******************************/ /*! * # Fomantic-UI - Modal * http://github.com/fomantic/Fomantic-UI/ @@ -16722,7 +9495,6 @@ Floated Menu / Item margin: -0.64285714em 0 0 -0.64285714em; width: 1.28571429em; height: 1.28571429em; - -webkit-animation: loader 0.6s infinite linear; animation: loader 0.6s infinite linear; border: 0.2em solid #767676; border-radius: 500rem; @@ -16904,7 +9676,6 @@ Floated Menu / Item .ui.search.short > .results { overflow-x: hidden; overflow-y: auto; - -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-overflow-scrolling: touch; } @@ -17077,853 +9848,6 @@ Floated Menu / Item Theme Overrides *******************************/ -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Segment - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Segment -*******************************/ - -.ui.segment { - position: relative; - background: #FFFFFF; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15); - margin: 1rem 0; - padding: 1em 1em; - border-radius: 0.28571429rem; - border: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.segment:first-child { - margin-top: 0; -} - -.ui.segment:last-child { - margin-bottom: 0; -} - -/* Vertical */ - -.ui.vertical.segment { - margin: 0; - padding-left: 0; - padding-right: 0; - background: none transparent; - border-radius: 0; - box-shadow: none; - border: none; - border-bottom: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.vertical.segment:last-child { - border-bottom: none; -} - -/*------------------- - Loose Coupling ---------------------*/ - -/* Label */ - -.ui[class*="bottom attached"].segment > [class*="top attached"].label { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.ui[class*="top attached"].segment > [class*="bottom attached"].label { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.ui.attached.segment:not(.top):not(.bottom) > [class*="top attached"].label { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.ui.attached.segment:not(.top):not(.bottom) > [class*="bottom attached"].label { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -/* Grid */ - -.ui.page.grid.segment, -.ui.grid > .row > .ui.segment.column, -.ui.grid > .ui.segment.column { - padding-top: 2em; - padding-bottom: 2em; -} - -.ui.grid.segment { - margin: 1rem 0; - border-radius: 0.28571429rem; -} - -/* Table */ - -.ui.basic.table.segment { - background: #FFFFFF; - border: 1px solid rgba(34, 36, 38, 0.15); - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15); -} - -.ui[class*="very basic"].table.segment { - padding: 1em 1em; -} - -/* Tab */ - -.ui.segment.tab:last-child { - margin-bottom: 1rem; -} - -/******************************* - Types -*******************************/ - -/*------------------- - Placeholder - --------------------*/ - -.ui.placeholder.segment { - display: flex; - flex-direction: column; - justify-content: center; - align-items: stretch; - max-width: initial; - -webkit-animation: none; - animation: none; - overflow: visible; - padding: 1em 1em; - min-height: 18rem; - background: #F9FAFB; - border-color: rgba(34, 36, 38, 0.15); - box-shadow: 0 2px 25px 0 rgba(34, 36, 38, 0.05) inset; -} - -.ui.placeholder.segment .button, -.ui.placeholder.segment textarea { - display: block; -} - -.ui.placeholder.segment .field, -.ui.placeholder.segment textarea, -.ui.placeholder.segment > .ui.input, -.ui.placeholder.segment .button { - max-width: 15rem; - margin-left: auto; - margin-right: auto; -} - -.ui.placeholder.segment .column .button, -.ui.placeholder.segment .column .field, -.ui.placeholder.segment .column textarea, -.ui.placeholder.segment .column > .ui.input { - max-width: 15rem; - margin-left: auto; - margin-right: auto; -} - -.ui.placeholder.segment > .inline { - align-self: center; -} - -.ui.placeholder.segment > .inline > .button { - display: inline-block; - width: auto; - margin: 0 0.35714286rem 0 0; -} - -.ui.placeholder.segment > .inline > .button:last-child { - margin-right: 0; -} - -/*------------------- - Padded - --------------------*/ - -.ui.padded.segment { - padding: 1.5em; -} - -.ui[class*="very padded"].segment { - padding: 3em; -} - -/* Padded vertical */ - -.ui.padded.segment.vertical.segment, -.ui[class*="very padded"].vertical.segment { - padding-left: 0; - padding-right: 0; -} - -/*------------------- - Compact - --------------------*/ - -.ui.compact.segment { - display: table; -} - -/* Compact Group */ - -.ui.compact.segments { - display: inline-flex; -} - -.ui.compact.segments .segment, -.ui.segments .compact.segment { - display: block; - flex: 0 1 auto; -} - -/*------------------- - Circular - --------------------*/ - -.ui.circular.segment { - display: table-cell; - padding: 2em; - text-align: center; - vertical-align: middle; - border-radius: 500em; -} - -/*------------------- - Raised - --------------------*/ - -.ui.raised.raised.segments, -.ui.raised.raised.segment { - box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15); -} - -/******************************* - Groups - *******************************/ - -/* Group */ - -.ui.segments { - flex-direction: column; - position: relative; - margin: 1rem 0; - border: 1px solid rgba(34, 36, 38, 0.15); - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15); - border-radius: 0.28571429rem; -} - -.ui.segments:first-child { - margin-top: 0; -} - -.ui.segments:last-child { - margin-bottom: 0; -} - -/* Nested Segment */ - -.ui.segments > .segment { - top: 0; - bottom: 0; - border-radius: 0; - margin: 0; - width: auto; - box-shadow: none; - border: none; - border-top: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.segments:not(.horizontal) > .segment:first-child { - top: 0; - bottom: 0; - border-top: none; - margin-top: 0; - margin-bottom: 0; - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -/* Bottom */ - -.ui.segments:not(.horizontal) > .segment:last-child { - top: 0; - bottom: 0; - margin-top: 0; - margin-bottom: 0; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), none; - border-radius: 0 0 0.28571429rem 0.28571429rem; -} - -/* Only */ - -.ui.segments:not(.horizontal) > .segment:only-child { - border-radius: 0.28571429rem; -} - -/* Nested Group */ - -.ui.segments > .ui.segments { - border-top: 1px solid rgba(34, 36, 38, 0.15); - margin: 1rem 1rem; -} - -.ui.segments > .segments:first-child { - border-top: none; -} - -.ui.segments > .segment + .segments:not(.horizontal) { - margin-top: 0; -} - -/* Horizontal Group */ - -.ui.horizontal.segments { - display: flex; - flex-direction: row; - background-color: transparent; - padding: 0; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15); - margin: 1rem 0; - border-radius: 0.28571429rem; - border: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.stackable.horizontal.segments { - flex-wrap: wrap; -} - -/* Nested Horizontal Group */ - -.ui.segments > .horizontal.segments { - margin: 0; - background-color: transparent; - border-radius: 0; - border: none; - box-shadow: none; - border-top: 1px solid rgba(34, 36, 38, 0.15); -} - -/* Horizontal Segment */ - -.ui.horizontal.segments:not(.compact) > .segment:not(.compact) { - flex: 1 1 auto; - -ms-flex: 1 1 0; - /* Solves #2550 MS Flex */ -} - -.ui.horizontal.segments > .segment { - margin: 0; - min-width: 0; - border-radius: 0; - border: none; - box-shadow: none; - border-left: 1px solid rgba(34, 36, 38, 0.15); -} - -/* Border Fixes */ - -.ui.segments > .horizontal.segments:first-child { - border-top: none; -} - -.ui.horizontal.segments:not(.stackable) > .segment:first-child { - border-left: none; -} - -.ui.horizontal.segments > .segment:first-child { - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui.horizontal.segments > .segment:last-child { - border-radius: 0 0.28571429rem 0.28571429rem 0; -} - -/******************************* - States -*******************************/ - -/*-------------- - Disabled - ---------------*/ - -.ui.disabled.segment { - opacity: var(--opacity-disabled); - color: rgba(40, 40, 40, 0.3); -} - -/*-------------- - Loading - ---------------*/ - -.ui.loading.segment { - position: relative; - cursor: default; - pointer-events: none; - text-shadow: none !important; - transition: all 0s linear; -} - -.ui.loading.segment:before { - position: absolute; - content: ''; - top: 0; - left: 0; - background: rgba(255, 255, 255, 0.8); - width: 100%; - height: 100%; - border-radius: 0.28571429rem; - z-index: 100; -} - -.ui.loading.segment:after { - position: absolute; - content: ''; - top: 50%; - left: 50%; - margin: -1.5em 0 0 -1.5em; - width: 3em; - height: 3em; - -webkit-animation: loader 0.6s infinite linear; - animation: loader 0.6s infinite linear; - border: 0.2em solid #767676; - border-radius: 500rem; - box-shadow: 0 0 0 1px transparent; - visibility: visible; - z-index: 101; -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Basic - --------------------*/ - -.ui.basic.segment, -.ui.segments .ui.basic.segment, -.ui.basic.segments { - background: none transparent; - box-shadow: none; - border: none; - border-radius: 0; -} - -/*------------------- - Clearing - --------------------*/ - -.ui.clearing.segment:after { - content: ""; - display: block; - clear: both; -} - -/*------------------- - Colors ---------------------*/ - -.ui.red.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #DB2828; -} - -.ui.orange.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #F2711C; -} - -.ui.yellow.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #FBBD08; -} - -.ui.olive.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #B5CC18; -} - -.ui.green.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #21BA45; -} - -.ui.teal.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #00B5AD; -} - -.ui.blue.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #2185D0; -} - -.ui.violet.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #6435C9; -} - -.ui.purple.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #A333C8; -} - -.ui.pink.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #E03997; -} - -.ui.brown.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #A5673F; -} - -.ui.grey.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #767676; -} - -.ui.black.segment.segment.segment.segment.segment:not(.inverted) { - border-top: 2px solid #1B1C1D; -} - -/*------------------- - Aligned - --------------------*/ - -.ui[class*="left aligned"].segment { - text-align: left; -} - -.ui[class*="right aligned"].segment { - text-align: right; -} - -.ui[class*="center aligned"].segment { - text-align: center; -} - -/*------------------- - Floated - --------------------*/ - -.ui.floated.segment, -.ui[class*="left floated"].segment { - float: left; - margin-right: 1em; -} - -.ui[class*="right floated"].segment { - float: right; - margin-left: 1em; -} - -/*------------------- - Emphasis ---------------------*/ - -/* Secondary */ - -.ui.secondary.segment { - background: #F3F4F5; - color: rgba(0, 0, 0, 0.6); -} - -/* Tertiary */ - -.ui.tertiary.segment { - background: #DCDDDE; - color: rgba(0, 0, 0, 0.6); -} - -/*------------------- - Attached - --------------------*/ - -/* Middle */ - -.ui.attached.segment { - top: 0; - bottom: 0; - border-radius: 0; - margin: 0 -1px; - width: calc(100% + 2px); - max-width: calc(100% + 2px); - box-shadow: none; - border: 1px solid #D4D4D5; -} - -.ui.attached:not(.message) + .ui.attached.segment:not(.top) { - border-top: none; -} - -/* Top */ - -.ui[class*="top attached"].segment { - bottom: 0; - margin-bottom: 0; - top: 0; - margin-top: 1rem; - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -.ui.segment[class*="top attached"]:first-child { - margin-top: 0; -} - -/* Bottom */ - -.ui.segment[class*="bottom attached"] { - bottom: 0; - margin-top: 0; - top: 0; - margin-bottom: 1rem; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), none; - border-radius: 0 0 0.28571429rem 0.28571429rem; -} - -.ui.segment[class*="bottom attached"]:last-child { - margin-bottom: 1rem; -} - -/*-------------- - Fitted - ---------------*/ - -.ui.fitted.segment:not(.horizontally) { - padding-top: 0; - padding-bottom: 0; -} - -.ui.fitted.segment:not(.vertically) { - padding-left: 0; - padding-right: 0; -} - -/*------------------- - Size ---------------------*/ - -.ui.segments .segment, -.ui.segment { - font-size: 1rem; -} - -.ui.mini.segments .segment, -.ui.mini.segment { - font-size: 0.78571429rem; -} - -.ui.tiny.segments .segment, -.ui.tiny.segment { - font-size: 0.85714286rem; -} - -.ui.small.segments .segment, -.ui.small.segment { - font-size: 0.92857143rem; -} - -.ui.large.segments .segment, -.ui.large.segment { - font-size: 1.14285714rem; -} - -.ui.big.segments .segment, -.ui.big.segment { - font-size: 1.28571429rem; -} - -.ui.huge.segments .segment, -.ui.huge.segment { - font-size: 1.42857143rem; -} - -.ui.massive.segments .segment, -.ui.massive.segment { - font-size: 1.71428571rem; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Site - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Page -*******************************/ - -html, -body { - height: 100%; -} - -html { - font-size: 14px; -} - -body { - margin: 0; - padding: 0; - overflow-x: visible; - min-width: 320px; - background: #FFFFFF; - font-family: var(--fonts-regular); - font-size: 14px; - line-height: 1.4285em; - color: rgba(0, 0, 0, 0.87); -} - -/******************************* - Headers -*******************************/ - -h1, -h2, -h3, -h4, -h5 { - font-family: var(--fonts-regular); - line-height: 1.28571429em; - margin: calc(2rem - 0.1428571428571429em) 0 1rem; - font-weight: 500; - padding: 0; -} - -h1 { - min-height: 1rem; - font-size: 2rem; -} - -h2 { - font-size: 1.71428571rem; -} - -h3 { - font-size: 1.28571429rem; -} - -h4 { - font-size: 1.07142857rem; -} - -h5 { - font-size: 1rem; -} - -h1:first-child, -h2:first-child, -h3:first-child, -h4:first-child, -h5:first-child { - margin-top: 0; -} - -h1:last-child, -h2:last-child, -h3:last-child, -h4:last-child, -h5:last-child { - margin-bottom: 0; -} - -/******************************* - Text -*******************************/ - -p { - margin: 0 0 1em; - line-height: 1.4285em; -} - -p:first-child { - margin-top: 0; -} - -p:last-child { - margin-bottom: 0; -} - -/*------------------- - Links ---------------------*/ - -a { - color: #4183C4; - text-decoration: none; -} - -a:hover { - color: #1e70bf; - text-decoration: underline; -} - -/******************************* - Scrollbars -*******************************/ - -/******************************* - Highlighting -*******************************/ - -/* Site */ - -::-webkit-selection { - background-color: #CCE2FF; - color: rgba(0, 0, 0, 0.87); -} - -::-moz-selection { - background-color: #CCE2FF; - color: rgba(0, 0, 0, 0.87); -} - -::selection { - background-color: #CCE2FF; - color: rgba(0, 0, 0, 0.87); -} - -/* Form */ - -textarea::-webkit-selection, -input::-webkit-selection { - background-color: rgba(100, 100, 100, 0.4); - color: rgba(0, 0, 0, 0.87); -} - -textarea::-moz-selection, -input::-moz-selection { - background-color: rgba(100, 100, 100, 0.4); - color: rgba(0, 0, 0, 0.87); -} - -textarea::-moz-selection, -input::-moz-selection { - background-color: rgba(100, 100, 100, 0.4); - color: rgba(0, 0, 0, 0.87); -} - -textarea::selection, -input::selection { - background-color: rgba(100, 100, 100, 0.4); - color: rgba(0, 0, 0, 0.87); -} - -/******************************* - Global Overrides -*******************************/ - /******************************* Site Overrides *******************************/ @@ -17996,7 +9920,6 @@ input::selection { margin: -1.25em 0 0 -1.25em; width: 2.5em; height: 2.5em; - -webkit-animation: loader 0.6s infinite linear; animation: loader 0.6s infinite linear; border: 0.2em solid #767676; border-radius: 500rem; @@ -18009,1366 +9932,4 @@ input::selection { /******************************* User Overrides -*******************************/ -/*! - * # Fomantic-UI - Table - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Table -*******************************/ - -/* Prototype */ - -.ui.table { - width: 100%; - background: #FFFFFF; - margin: 1em 0; - border: 1px solid rgba(34, 36, 38, 0.15); - box-shadow: none; - border-radius: 0.28571429rem; - text-align: left; - vertical-align: middle; - color: rgba(0, 0, 0, 0.87); - border-collapse: separate; - border-spacing: 0; -} - -.ui.table:first-child { - margin-top: 0; -} - -.ui.table:last-child { - margin-bottom: 0; -} - -.ui.table > thead, -.ui.table > tbody { - text-align: inherit; - vertical-align: inherit; -} - -/******************************* - Parts -*******************************/ - -/* Table Content */ - -.ui.table th, -.ui.table td { - transition: background 0.1s ease, color 0.1s ease; -} - -/* Rowspan helper class */ - -.ui.table th.rowspanned, -.ui.table td.rowspanned { - display: none; -} - -/* Headers */ - -.ui.table > thead { - box-shadow: none; -} - -.ui.table > thead > tr > th { - cursor: auto; - background: #F9FAFB; - text-align: inherit; - color: rgba(0, 0, 0, 0.87); - padding: 0.92857143em 0.78571429em; - vertical-align: inherit; - font-style: none; - font-weight: 500; - text-transform: none; - border-bottom: 1px solid rgba(34, 36, 38, 0.1); - border-left: none; -} - -.ui.table > thead > tr > th:first-child { - border-left: none; -} - -.ui.table > thead > tr:first-child > th:first-child { - border-radius: 0.28571429rem 0 0 0; -} - -.ui.table > thead > tr:first-child > th:last-child { - border-radius: 0 0.28571429rem 0 0; -} - -.ui.table > thead > tr:first-child > th:only-child { - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -/* Footer */ - -.ui.table > tfoot { - box-shadow: none; -} - -.ui.table > tfoot > tr > th, -.ui.table > tfoot > tr > td { - cursor: auto; - border-top: 1px solid rgba(34, 36, 38, 0.15); - background: #F9FAFB; - text-align: inherit; - color: rgba(0, 0, 0, 0.87); - padding: 0.78571429em 0.78571429em; - vertical-align: inherit; - font-style: normal; - font-weight: normal; - text-transform: none; -} - -.ui.table > tfoot > tr > th:first-child, -.ui.table > tfoot > tr > td:first-child { - border-left: none; -} - -.ui.table > tfoot > tr:first-child > th:first-child, -.ui.table > tfoot > tr:first-child > td:first-child { - border-radius: 0 0 0 0.28571429rem; -} - -.ui.table > tfoot > tr:first-child > th:last-child, -.ui.table > tfoot > tr:first-child > td:last-child { - border-radius: 0 0 0.28571429rem 0; -} - -.ui.table > tfoot > tr:first-child > th:only-child, -.ui.table > tfoot > tr:first-child > td:only-child { - border-radius: 0 0 0.28571429rem 0.28571429rem; -} - -/* Table Row */ - -.ui.table > tr > td, -.ui.table > tbody > tr > td { - border-top: 1px solid rgba(34, 36, 38, 0.1); -} - -.ui.table > tr:first-child > td, -.ui.table > tbody > tr:first-child > td { - border-top: none; -} - -/* Repeated tbody */ - -.ui.table > tbody + tbody tr:first-child > td { - border-top: 1px solid rgba(34, 36, 38, 0.1); -} - -/* Table Cells */ - -.ui.table > tbody > tr > td, -.ui.table > tr > td { - padding: 0.78571429em 0.78571429em; - text-align: inherit; -} - -/* Icons */ - -.ui.table > i.icon { - vertical-align: baseline; -} - -.ui.table > i.icon:only-child { - margin: 0; -} - -/* Table Segment */ - -.ui.table.segment { - padding: 0; -} - -.ui.table.segment:after { - display: none; -} - -.ui.table.segment.stacked:after { - display: block; -} - -/* Responsive */ - -@media only screen and (max-width: 767.98px) { - .ui.table:not(.unstackable) { - width: 100%; - padding: 0; - } - - .ui.table:not(.unstackable) > thead, - .ui.table:not(.unstackable) > thead > tr, - .ui.table:not(.unstackable) > tfoot, - .ui.table:not(.unstackable) > tfoot > tr, - .ui.table:not(.unstackable) > tbody, - .ui.table:not(.unstackable) > tr, - .ui.table:not(.unstackable) > tbody > tr, - .ui.table:not(.unstackable) > tr > th:not(.rowspanned), - .ui.table:not(.unstackable) > thead > tr > th:not(.rowspanned), - .ui.table:not(.unstackable) > tbody > tr > th:not(.rowspanned), - .ui.table:not(.unstackable) > tfoot > tr > th:not(.rowspanned), - .ui.table:not(.unstackable) > tr > td:not(.rowspanned), - .ui.table:not(.unstackable) > tbody > tr > td:not(.rowspanned), - .ui.table:not(.unstackable) > tfoot > tr > td:not(.rowspanned) { - display: block !important; - width: auto !important; - } - - .ui.table:not(.unstackable) > thead { - display: block; - } - - .ui.table:not(.unstackable) > tfoot { - display: block; - } - - .ui.ui.ui.ui.table:not(.unstackable) > tr, - .ui.ui.ui.ui.table:not(.unstackable) > thead > tr, - .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr, - .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr { - padding-top: 1em; - padding-bottom: 1em; - box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.1) inset; - } - - .ui.ui.ui.ui.table:not(.unstackable) > tr > th, - .ui.ui.ui.ui.table:not(.unstackable) > thead > tr > th, - .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > th, - .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > th, - .ui.ui.ui.ui.table:not(.unstackable) > tr > td, - .ui.ui.ui.ui.table:not(.unstackable) > tbody > tr > td, - .ui.ui.ui.ui.table:not(.unstackable) > tfoot > tr > td { - background: none; - border: none; - padding: 0.25em 0.75em; - box-shadow: none; - } - - .ui.table:not(.unstackable) > tr > th:first-child, - .ui.table:not(.unstackable) > thead > tr > th:first-child, - .ui.table:not(.unstackable) > tbody > tr > th:first-child, - .ui.table:not(.unstackable) > tfoot > tr > th:first-child, - .ui.table:not(.unstackable) > tr > td:first-child, - .ui.table:not(.unstackable) > tbody > tr > td:first-child, - .ui.table:not(.unstackable) > tfoot > tr > td:first-child { - font-weight: 500; - } - - /* Definition Table */ - - .ui.definition.table:not(.unstackable) > thead > tr > th:first-child { - box-shadow: none !important; - } -} - -/******************************* - Coupling -*******************************/ - -/* UI Image */ - -.ui.table .collapsing .image, -.ui.table .collapsing .image img { - max-width: none; -} - -/******************************* - Types -*******************************/ - -/*-------------- - Complex ----------------*/ - -.ui.structured.table { - border-collapse: collapse; -} - -.ui.structured.table > thead > tr > th { - border-left: none; - border-right: none; -} - -.ui.structured.sortable.table > thead > tr > th { - border-left: 1px solid rgba(34, 36, 38, 0.15); - border-right: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.structured.basic.table > tr > th, -.ui.structured.basic.table > thead > tr > th, -.ui.structured.basic.table > tbody > tr > th, -.ui.structured.basic.table > tfoot > tr > th { - border-left: none; - border-right: none; -} - -.ui.structured.celled.table > tr > th, -.ui.structured.celled.table > thead > tr > th, -.ui.structured.celled.table > tbody > tr > th, -.ui.structured.celled.table > tfoot > tr > th, -.ui.structured.celled.table > tr > td, -.ui.structured.celled.table > tbody > tr > td, -.ui.structured.celled.table > tfoot > tr > td { - border-left: 1px solid rgba(34, 36, 38, 0.1); - border-right: 1px solid rgba(34, 36, 38, 0.1); -} - -/*-------------- - Definition - ---------------*/ - -.ui.definition.table > thead:not(.full-width) > tr > th:first-child { - pointer-events: none; - background: #FFFFFF; - font-weight: normal; - color: rgba(0, 0, 0, 0.4); - box-shadow: -0.1em -0.2em 0 0.1em #FFFFFF; - -moz-transform: scale(1); -} - -.ui.definition.table > tfoot:not(.full-width) > tr > th:first-child { - pointer-events: none; - background: #FFFFFF; - font-weight: normal; - color: rgba(0, 0, 0, 0.4); - box-shadow: -0.1em 0.2em 0 0.1em #FFFFFF; - -moz-transform: scale(1); -} - -/* Highlight Defining Column */ - -.ui.definition.table > tr > td:first-child:not(.ignored), -.ui.definition.table > tbody > tr > td:first-child:not(.ignored), -.ui.definition.table > tfoot > tr > td:first-child:not(.ignored), -.ui.definition.table tr td.definition { - background: rgba(0, 0, 0, 0.03); - font-weight: 500; - color: rgba(0, 0, 0, 0.95); - text-transform: ''; - box-shadow: ''; - text-align: ''; - font-size: 1em; - padding-left: ''; - padding-right: ''; -} - -/* Fix 2nd Column */ - -.ui.definition.table > thead:not(.full-width) > tr > th:nth-child(2) { - border-left: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.definition.table > tfoot:not(.full-width) > tr > th:nth-child(2), -.ui.definition.table > tfoot:not(.full-width) > tr > td:nth-child(2) { - border-left: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.definition.table > tr > td:nth-child(2), -.ui.definition.table > tbody > tr > td:nth-child(2) { - border-left: 1px solid rgba(34, 36, 38, 0.15); -} - -/******************************* - States -*******************************/ - -/*-------------- - Positive - ---------------*/ - -.ui.ui.ui.ui.table tr.positive, -.ui.ui.table td.positive { - box-shadow: 0 0 0 #A3C293 inset; - background: #FCFFF5; - color: #2C662D; -} - -/*-------------- - Negative - ---------------*/ - -.ui.ui.ui.ui.table tr.negative, -.ui.ui.table td.negative { - box-shadow: 0 0 0 #E0B4B4 inset; - background: #FFF6F6; - color: #9F3A38; -} - -/*-------------- - Error - ---------------*/ - -.ui.ui.ui.ui.table tr.error, -.ui.ui.table td.error { - box-shadow: 0 0 0 #E0B4B4 inset; - background: #FFF6F6; - color: #9F3A38; -} - -/*-------------- - Warning - ---------------*/ - -.ui.ui.ui.ui.table tr.warning, -.ui.ui.table td.warning { - box-shadow: 0 0 0 #C9BA9B inset; - background: #FFFAF3; - color: #573A08; -} - -/*-------------- - Active - ---------------*/ - -.ui.ui.ui.ui.table tr.active, -.ui.ui.table td.active { - box-shadow: 0 0 0 rgba(0, 0, 0, 0.87) inset; - background: #E0E0E0; - color: rgba(0, 0, 0, 0.87); -} - -/*-------------- - Disabled - ---------------*/ - -.ui.table tr.disabled td, -.ui.table tr td.disabled, -.ui.table tr.disabled:hover, -.ui.table tr:hover td.disabled { - pointer-events: none; - color: rgba(40, 40, 40, 0.3); -} - -/******************************* - Variations -*******************************/ - -/*-------------- - Text Alignment - ---------------*/ - -.ui.table[class*="left aligned"], -.ui.table [class*="left aligned"] { - text-align: left; -} - -.ui.table[class*="center aligned"], -.ui.table [class*="center aligned"] { - text-align: center; -} - -.ui.table[class*="right aligned"], -.ui.table [class*="right aligned"] { - text-align: right; -} - -/*------------------ - Vertical Alignment - ------------------*/ - -.ui.table[class*="top aligned"], -.ui.table [class*="top aligned"] { - vertical-align: top; -} - -.ui.table[class*="middle aligned"], -.ui.table [class*="middle aligned"] { - vertical-align: middle; -} - -.ui.table[class*="bottom aligned"], -.ui.table [class*="bottom aligned"] { - vertical-align: bottom; -} - -/*-------------- - Collapsing - ---------------*/ - -.ui.table th.collapsing, -.ui.table td.collapsing { - width: 1px; - white-space: nowrap; -} - -/*-------------- - Fixed - ---------------*/ - -.ui.fixed.table { - table-layout: fixed; -} - -.ui.fixed.table th, -.ui.fixed.table td { - overflow: hidden; - text-overflow: ellipsis; -} - -/*-------------- - Selectable - ---------------*/ - -.ui.ui.selectable.table > tbody > tr:hover, -.ui.table tbody tr td.selectable:hover { - background: rgba(0, 0, 0, 0.05); - color: rgba(0, 0, 0, 0.95); -} - -/* Selectable Cell Link */ - -.ui.table tbody tr td.selectable { - padding: 0; -} - -.ui.table tbody tr td.selectable > a:not(.ui) { - display: block; - color: inherit; - padding: 0.78571429em 0.78571429em; -} - -.ui.table > tr > td.selectable, -.ui.table > tbody > tr > td.selectable, -.ui.selectable.table > tbody > tr, -.ui.selectable.table > tr { - cursor: pointer; -} - -/* Other States */ - -.ui.ui.selectable.table tr.error:hover, -.ui.table tr td.selectable.error:hover, -.ui.selectable.table tr:hover td.error { - background: #ffe7e7; - color: #943634; -} - -.ui.ui.selectable.table tr.warning:hover, -.ui.table tr td.selectable.warning:hover, -.ui.selectable.table tr:hover td.warning { - background: #fff4e4; - color: #493107; -} - -.ui.ui.selectable.table tr.active:hover, -.ui.table tr td.selectable.active:hover, -.ui.selectable.table tr:hover td.active { - background: #E0E0E0; - color: rgba(0, 0, 0, 0.87); -} - -.ui.ui.selectable.table tr.positive:hover, -.ui.table tr td.selectable.positive:hover, -.ui.selectable.table tr:hover td.positive { - background: #f7ffe6; - color: #275b28; -} - -.ui.ui.selectable.table tr.negative:hover, -.ui.table tr td.selectable.negative:hover, -.ui.selectable.table tr:hover td.negative { - background: #ffe7e7; - color: #943634; -} - -/*------------------- - Attached - --------------------*/ - -/* Middle */ - -.ui.attached.table { - top: 0; - bottom: 0; - border-radius: 0; - margin: 0 -1px; - width: calc(100% + 2px); - max-width: calc(100% + 2px); - box-shadow: none; - border: 1px solid #D4D4D5; -} - -.ui.attached + .ui.attached.table:not(.top) { - border-top: none; -} - -/* Top */ - -.ui[class*="top attached"].table { - bottom: 0; - margin-bottom: 0; - top: 0; - margin-top: 1em; - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -.ui.table[class*="top attached"]:first-child { - margin-top: 0; -} - -/* Bottom */ - -.ui[class*="bottom attached"].table { - bottom: 0; - margin-top: 0; - top: 0; - margin-bottom: 1em; - box-shadow: none, none; - border-radius: 0 0 0.28571429rem 0.28571429rem; -} - -.ui[class*="bottom attached"].table:last-child { - margin-bottom: 0; -} - -/*-------------- - Striped - ---------------*/ - -/* Table Striping */ - -.ui.striped.table > tr:nth-child(2n), -.ui.striped.table > tbody > tr:nth-child(2n) { - background-color: rgba(0, 0, 50, 0.02); -} - -/* Allow striped active hover */ - -.ui.striped.selectable.selectable.selectable.table tbody tr.active:hover { - background: #EFEFEF; - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Single Line ----------------*/ - -.ui.table[class*="single line"], -.ui.table [class*="single line"] { - white-space: nowrap; -} - -/*------------------- - Colors ---------------------*/ - -.ui.primary.table { - border-top: 0.2em solid #2185D0; -} - -.ui.ui.ui.ui.table tr.primary:not(.marked), -.ui.ui.table td.primary:not(.marked) { - background: #ddf4ff; - color: rgba(255, 255, 255, 0.9); -} - -.ui.ui.selectable.table tr.primary:not(.marked):hover, -.ui.table tr td.selectable.primary:not(.marked):hover, -.ui.selectable.table tr:hover td.primary:not(.marked) { - background: #d3f1ff; - color: rgba(255, 255, 255, 0.9); -} - -.ui.secondary.table { - border-top: 0.2em solid #1B1C1D; -} - -.ui.ui.ui.ui.table tr.secondary:not(.marked), -.ui.ui.table td.secondary:not(.marked) { - background: #dddddd; - color: rgba(255, 255, 255, 0.9); -} - -.ui.ui.selectable.table tr.secondary:not(.marked):hover, -.ui.table tr td.selectable.secondary:not(.marked):hover, -.ui.selectable.table tr:hover td.secondary:not(.marked) { - background: #e2e2e2; - color: rgba(255, 255, 255, 0.9); -} - -.ui.red.table { - border-top: 0.2em solid #DB2828; -} - -.ui.ui.ui.ui.table tr.red:not(.marked), -.ui.ui.table td.red:not(.marked) { - background: #ffe1df; - color: #DB2828; -} - -.ui.ui.selectable.table tr.red:not(.marked):hover, -.ui.table tr td.selectable.red:not(.marked):hover, -.ui.selectable.table tr:hover td.red:not(.marked) { - background: #ffd7d5; - color: #DB2828; -} - -.ui.orange.table { - border-top: 0.2em solid #F2711C; -} - -.ui.ui.ui.ui.table tr.orange:not(.marked), -.ui.ui.table td.orange:not(.marked) { - background: #ffe7d1; - color: #F2711C; -} - -.ui.ui.selectable.table tr.orange:not(.marked):hover, -.ui.table tr td.selectable.orange:not(.marked):hover, -.ui.selectable.table tr:hover td.orange:not(.marked) { - background: #fae1cc; - color: #F2711C; -} - -.ui.yellow.table { - border-top: 0.2em solid #FBBD08; -} - -.ui.ui.ui.ui.table tr.yellow:not(.marked), -.ui.ui.table td.yellow:not(.marked) { - background: #fff9d2; - color: #B58105; -} - -.ui.ui.selectable.table tr.yellow:not(.marked):hover, -.ui.table tr td.selectable.yellow:not(.marked):hover, -.ui.selectable.table tr:hover td.yellow:not(.marked) { - background: #fbf5cc; - color: #B58105; -} - -.ui.olive.table { - border-top: 0.2em solid #B5CC18; -} - -.ui.ui.ui.ui.table tr.olive:not(.marked), -.ui.ui.table td.olive:not(.marked) { - background: #f7fae4; - color: #8ABC1E; -} - -.ui.ui.selectable.table tr.olive:not(.marked):hover, -.ui.table tr td.selectable.olive:not(.marked):hover, -.ui.selectable.table tr:hover td.olive:not(.marked) { - background: #f6fada; - color: #8ABC1E; -} - -.ui.green.table { - border-top: 0.2em solid #21BA45; -} - -.ui.ui.ui.ui.table tr.green:not(.marked), -.ui.ui.table td.green:not(.marked) { - background: #d5f5d9; - color: #1EBC30; -} - -.ui.ui.selectable.table tr.green:not(.marked):hover, -.ui.table tr td.selectable.green:not(.marked):hover, -.ui.selectable.table tr:hover td.green:not(.marked) { - background: #d2eed5; - color: #1EBC30; -} - -.ui.teal.table { - border-top: 0.2em solid #00B5AD; -} - -.ui.ui.ui.ui.table tr.teal:not(.marked), -.ui.ui.table td.teal:not(.marked) { - background: #e2ffff; - color: #10A3A3; -} - -.ui.ui.selectable.table tr.teal:not(.marked):hover, -.ui.table tr td.selectable.teal:not(.marked):hover, -.ui.selectable.table tr:hover td.teal:not(.marked) { - background: #d8ffff; - color: #10A3A3; -} - -.ui.blue.table { - border-top: 0.2em solid #2185D0; -} - -.ui.ui.ui.ui.table tr.blue:not(.marked), -.ui.ui.table td.blue:not(.marked) { - background: #ddf4ff; - color: #2185D0; -} - -.ui.ui.selectable.table tr.blue:not(.marked):hover, -.ui.table tr td.selectable.blue:not(.marked):hover, -.ui.selectable.table tr:hover td.blue:not(.marked) { - background: #d3f1ff; - color: #2185D0; -} - -.ui.violet.table { - border-top: 0.2em solid #6435C9; -} - -.ui.ui.ui.ui.table tr.violet:not(.marked), -.ui.ui.table td.violet:not(.marked) { - background: #ece9fe; - color: #6435C9; -} - -.ui.ui.selectable.table tr.violet:not(.marked):hover, -.ui.table tr td.selectable.violet:not(.marked):hover, -.ui.selectable.table tr:hover td.violet:not(.marked) { - background: #e3deff; - color: #6435C9; -} - -.ui.purple.table { - border-top: 0.2em solid #A333C8; -} - -.ui.ui.ui.ui.table tr.purple:not(.marked), -.ui.ui.table td.purple:not(.marked) { - background: #f8e3ff; - color: #A333C8; -} - -.ui.ui.selectable.table tr.purple:not(.marked):hover, -.ui.table tr td.selectable.purple:not(.marked):hover, -.ui.selectable.table tr:hover td.purple:not(.marked) { - background: #f5d9ff; - color: #A333C8; -} - -.ui.pink.table { - border-top: 0.2em solid #E03997; -} - -.ui.ui.ui.ui.table tr.pink:not(.marked), -.ui.ui.table td.pink:not(.marked) { - background: #ffe8f9; - color: #E03997; -} - -.ui.ui.selectable.table tr.pink:not(.marked):hover, -.ui.table tr td.selectable.pink:not(.marked):hover, -.ui.selectable.table tr:hover td.pink:not(.marked) { - background: #ffdef6; - color: #E03997; -} - -.ui.brown.table { - border-top: 0.2em solid #A5673F; -} - -.ui.ui.ui.ui.table tr.brown:not(.marked), -.ui.ui.table td.brown:not(.marked) { - background: #f7e5d2; - color: #A5673F; -} - -.ui.ui.selectable.table tr.brown:not(.marked):hover, -.ui.table tr td.selectable.brown:not(.marked):hover, -.ui.selectable.table tr:hover td.brown:not(.marked) { - background: #efe0cf; - color: #A5673F; -} - -.ui.grey.table { - border-top: 0.2em solid #767676; -} - -.ui.ui.ui.ui.table tr.grey:not(.marked), -.ui.ui.table td.grey:not(.marked) { - background: #DCDDDE; - color: #767676; -} - -.ui.ui.selectable.table tr.grey:not(.marked):hover, -.ui.table tr td.selectable.grey:not(.marked):hover, -.ui.selectable.table tr:hover td.grey:not(.marked) { - background: #c2c4c5; - color: #767676; -} - -.ui.black.table { - border-top: 0.2em solid #1B1C1D; -} - -.ui.ui.ui.ui.table tr.black:not(.marked), -.ui.ui.table td.black:not(.marked) { - background: #545454; - color: #FFFFFF; -} - -.ui.ui.selectable.table tr.black:not(.marked):hover, -.ui.table tr td.selectable.black:not(.marked):hover, -.ui.selectable.table tr:hover td.black:not(.marked) { - background: #000000; - color: #FFFFFF; -} - -/*-------------- - Column Count ----------------*/ - -/* Grid Based */ - -.ui.one.column.table td { - width: 100%; -} - -.ui.two.column.table td { - width: 50%; -} - -.ui.three.column.table td { - width: 33.33333333%; -} - -.ui.four.column.table td { - width: 25%; -} - -.ui.five.column.table td { - width: 20%; -} - -.ui.six.column.table td { - width: 16.66666667%; -} - -.ui.seven.column.table td { - width: 14.28571429%; -} - -.ui.eight.column.table td { - width: 12.5%; -} - -.ui.nine.column.table td { - width: 11.11111111%; -} - -.ui.ten.column.table td { - width: 10%; -} - -.ui.eleven.column.table td { - width: 9.09090909%; -} - -.ui.twelve.column.table td { - width: 8.33333333%; -} - -.ui.thirteen.column.table td { - width: 7.69230769%; -} - -.ui.fourteen.column.table td { - width: 7.14285714%; -} - -.ui.fifteen.column.table td { - width: 6.66666667%; -} - -.ui.sixteen.column.table td { - width: 6.25%; -} - -/* Column Width */ - -.ui.table th.one.wide, -.ui.table td.one.wide { - width: 6.25%; -} - -.ui.table th.two.wide, -.ui.table td.two.wide { - width: 12.5%; -} - -.ui.table th.three.wide, -.ui.table td.three.wide { - width: 18.75%; -} - -.ui.table th.four.wide, -.ui.table td.four.wide { - width: 25%; -} - -.ui.table th.five.wide, -.ui.table td.five.wide { - width: 31.25%; -} - -.ui.table th.six.wide, -.ui.table td.six.wide { - width: 37.5%; -} - -.ui.table th.seven.wide, -.ui.table td.seven.wide { - width: 43.75%; -} - -.ui.table th.eight.wide, -.ui.table td.eight.wide { - width: 50%; -} - -.ui.table th.nine.wide, -.ui.table td.nine.wide { - width: 56.25%; -} - -.ui.table th.ten.wide, -.ui.table td.ten.wide { - width: 62.5%; -} - -.ui.table th.eleven.wide, -.ui.table td.eleven.wide { - width: 68.75%; -} - -.ui.table th.twelve.wide, -.ui.table td.twelve.wide { - width: 75%; -} - -.ui.table th.thirteen.wide, -.ui.table td.thirteen.wide { - width: 81.25%; -} - -.ui.table th.fourteen.wide, -.ui.table td.fourteen.wide { - width: 87.5%; -} - -.ui.table th.fifteen.wide, -.ui.table td.fifteen.wide { - width: 93.75%; -} - -.ui.table th.sixteen.wide, -.ui.table td.sixteen.wide { - width: 100%; -} - -/*-------------- - Sortable - ---------------*/ - -.ui.sortable.table > thead > tr > th { - cursor: pointer; - white-space: nowrap; - border-left: 1px solid rgba(34, 36, 38, 0.15); - color: rgba(0, 0, 0, 0.87); -} - -.ui.sortable.table > thead > tr > th:first-child { - border-left: none; -} - -.ui.sortable.table thead th.sorted, -.ui.sortable.table thead th.sorted:hover { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.ui.sortable.table > thead > tr > th:after { - display: none; - font-style: normal; - font-weight: normal; - text-decoration: inherit; - content: ''; - height: 1em; - width: auto; - opacity: 0.8; - margin: 0 0 0 0.5em; - font-family: 'Icons'; -} - -.ui.sortable.table thead th.ascending:after { - content: '\f0d8'; -} - -.ui.sortable.table thead th.descending:after { - content: '\f0d7'; -} - -/* Hover */ - -.ui.sortable.table th.disabled:hover { - cursor: auto; - color: rgba(40, 40, 40, 0.3); -} - -.ui.sortable.table > thead > tr > th:hover { - color: rgba(0, 0, 0, 0.8); -} - -.ui.sortable.table:not(.basic) > thead > tr > th:hover { - background: rgba(0, 0, 0, 0.05); -} - -/* Sorted */ - -.ui.sortable.table thead th.sorted { - color: rgba(0, 0, 0, 0.95); -} - -.ui.sortable.table:not(.basic) thead th.sorted { - background: rgba(0, 0, 0, 0.05); -} - -.ui.sortable.table thead th.sorted:after { - display: inline-block; -} - -/* Sorted Hover */ - -.ui.sortable.table thead th.sorted:hover { - color: rgba(0, 0, 0, 0.95); -} - -.ui.sortable.table:not(.basic) thead th.sorted:hover { - background: rgba(0, 0, 0, 0.05); -} - -/*-------------- - Collapsing - ---------------*/ - -.ui.collapsing.table { - width: auto; -} - -/*-------------- - Basic - ---------------*/ - -.ui.basic.table { - background: transparent; - border: 1px solid rgba(34, 36, 38, 0.15); - box-shadow: none; -} - -.ui.basic.table > thead, -.ui.basic.table > tfoot { - box-shadow: none; -} - -.ui.basic.table > thead > tr > th, -.ui.basic.table > tbody > tr > th, -.ui.basic.table > tfoot > tr > th, -.ui.basic.table > tr > th { - background: transparent; - border-left: none; -} - -.ui.basic.table > tbody > tr { - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - -.ui.basic.table > tbody > tr > td, -.ui.basic.table > tfoot > tr > td, -.ui.basic.table > tr > td { - background: transparent; -} - -.ui.basic.striped.table > tbody > tr:nth-child(2n) { - background-color: rgba(0, 0, 0, 0.05); -} - -/* Very Basic */ - -.ui[class*="very basic"].table { - border: none; -} - -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td { - padding: ''; -} - -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th:first-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th:first-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th:first-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th:first-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td:first-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td:first-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > td:first-child { - padding-left: 0; -} - -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > th:last-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr > th:last-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > th:last-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > th:last-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tr > td:last-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tbody > tr > td:last-child, -.ui[class*="very basic"].table:not(.sortable):not(.striped) > tfoot > tr > td:last-child { - padding-right: 0; -} - -.ui[class*="very basic"].table:not(.sortable):not(.striped) > thead > tr:first-child > th { - padding-top: 0; -} - -/*-------------- - Celled - ---------------*/ - -.ui.celled.table > tr > th, -.ui.celled.table > thead > tr > th, -.ui.celled.table > tbody > tr > th, -.ui.celled.table > tfoot > tr > th, -.ui.celled.table > tr > td, -.ui.celled.table > tbody > tr > td, -.ui.celled.table > tfoot > tr > td { - border-left: 1px solid rgba(34, 36, 38, 0.1); -} - -.ui.celled.table > tr > th:first-child, -.ui.celled.table > thead > tr > th:first-child, -.ui.celled.table > tbody > tr > th:first-child, -.ui.celled.table > tfoot > tr > th:first-child, -.ui.celled.table > tr > td:first-child, -.ui.celled.table > tbody > tr > td:first-child, -.ui.celled.table > tfoot > tr > td:first-child { - border-left: none; -} - -/*-------------- - Padded - ---------------*/ - -.ui.padded.table > tr > th, -.ui.padded.table > thead > tr > th, -.ui.padded.table > tbody > tr > th, -.ui.padded.table > tfoot > tr > th { - padding-left: 1em; - padding-right: 1em; -} - -.ui.padded.table > tr > th, -.ui.padded.table > thead > tr > th, -.ui.padded.table > tbody > tr > th, -.ui.padded.table > tfoot > tr > th, -.ui.padded.table > tr > td, -.ui.padded.table > tbody > tr > td, -.ui.padded.table > tfoot > tr > td { - padding: 1em 1em; -} - -/* Very */ - -.ui[class*="very padded"].table > tr > th, -.ui[class*="very padded"].table > thead > tr > th, -.ui[class*="very padded"].table > tbody > tr > th, -.ui[class*="very padded"].table > tfoot > tr > th { - padding-left: 1.5em; - padding-right: 1.5em; -} - -.ui[class*="very padded"].table > tr > td, -.ui[class*="very padded"].table > tbody > tr > td, -.ui[class*="very padded"].table > tfoot > tr > td { - padding: 1.5em 1.5em; -} - -/*-------------- - Compact - ---------------*/ - -.ui.compact.table > tr > th, -.ui.compact.table > thead > tr > th, -.ui.compact.table > tbody > tr > th, -.ui.compact.table > tfoot > tr > th { - padding-left: 0.7em; - padding-right: 0.7em; -} - -.ui.compact.table > tr > td, -.ui.compact.table > tbody > tr > td, -.ui.compact.table > tfoot > tr > td { - padding: 0.5em 0.7em; -} - -/* Very */ - -.ui[class*="very compact"].table > tr > th, -.ui[class*="very compact"].table > thead > tr > th, -.ui[class*="very compact"].table > tbody > tr > th, -.ui[class*="very compact"].table > tfoot > tr > th { - padding-left: 0.6em; - padding-right: 0.6em; -} - -.ui[class*="very compact"].table > tr > td, -.ui[class*="very compact"].table > tbody > tr > td, -.ui[class*="very compact"].table > tfoot > tr > td { - padding: 0.4em 0.6em; -} - -/*-------------- - Sizes ----------------*/ - -/* Standard */ - -.ui.table { - font-size: 1em; -} - -.ui.mini.table { - font-size: 0.78571429rem; -} - -.ui.tiny.table { - font-size: 0.85714286rem; -} - -.ui.small.table { - font-size: 0.9em; -} - -.ui.large.table { - font-size: 1.1em; -} - -.ui.big.table { - font-size: 1.28571429rem; -} - -.ui.huge.table { - font-size: 1.42857143rem; -} - -.ui.massive.table { - font-size: 1.71428571rem; -} - -/******************************* - Site Overrides *******************************/ \ No newline at end of file diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js index 2a05d94d72..c150c8d9db 100644 --- a/web_src/fomantic/build/semantic.js +++ b/web_src/fomantic/build/semantic.js @@ -1184,883 +1184,6 @@ $.api.settings = { -})( jQuery, window, document ); - -/*! - * # Fomantic-UI - Checkbox - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -;(function ($, window, document, undefined) { - -'use strict'; - -$.isFunction = $.isFunction || function(obj) { - return typeof obj === "function" && typeof obj.nodeType !== "number"; -}; - -window = (typeof window != 'undefined' && window.Math == Math) - ? window - : (typeof self != 'undefined' && self.Math == Math) - ? self - : Function('return this')() -; - -$.fn.checkbox = function(parameters) { - var - $allModules = $(this), - moduleSelector = $allModules.selector || '', - - time = new Date().getTime(), - performance = [], - - query = arguments[0], - methodInvoked = (typeof query == 'string'), - queryArguments = [].slice.call(arguments, 1), - returnedValue - ; - - $allModules - .each(function() { - var - settings = $.extend(true, {}, $.fn.checkbox.settings, parameters), - - className = settings.className, - namespace = settings.namespace, - selector = settings.selector, - error = settings.error, - - eventNamespace = '.' + namespace, - moduleNamespace = 'module-' + namespace, - - $module = $(this), - $label = $(this).children(selector.label), - $input = $(this).children(selector.input), - input = $input[0], - - initialLoad = false, - shortcutPressed = false, - instance = $module.data(moduleNamespace), - - observer, - element = this, - module - ; - - module = { - - initialize: function() { - module.verbose('Initializing checkbox', settings); - - module.create.label(); - module.bind.events(); - - module.set.tabbable(); - module.hide.input(); - - module.observeChanges(); - module.instantiate(); - module.setup(); - }, - - instantiate: function() { - module.verbose('Storing instance of module', module); - instance = module; - $module - .data(moduleNamespace, module) - ; - }, - - destroy: function() { - module.verbose('Destroying module'); - module.unbind.events(); - module.show.input(); - $module.removeData(moduleNamespace); - }, - - fix: { - reference: function() { - if( $module.is(selector.input) ) { - module.debug('Behavior called on <input> adjusting invoked element'); - $module = $module.closest(selector.checkbox); - module.refresh(); - } - } - }, - - setup: function() { - module.set.initialLoad(); - if( module.is.indeterminate() ) { - module.debug('Initial value is indeterminate'); - module.indeterminate(); - } - else if( module.is.checked() ) { - module.debug('Initial value is checked'); - module.check(); - } - else { - module.debug('Initial value is unchecked'); - module.uncheck(); - } - module.remove.initialLoad(); - }, - - refresh: function() { - $label = $module.children(selector.label); - $input = $module.children(selector.input); - input = $input[0]; - }, - - hide: { - input: function() { - module.verbose('Modifying <input> z-index to be unselectable'); - $input.addClass(className.hidden); - } - }, - show: { - input: function() { - module.verbose('Modifying <input> z-index to be selectable'); - $input.removeClass(className.hidden); - } - }, - - observeChanges: function() { - if('MutationObserver' in window) { - observer = new MutationObserver(function(mutations) { - module.debug('DOM tree modified, updating selector cache'); - module.refresh(); - }); - observer.observe(element, { - childList : true, - subtree : true - }); - module.debug('Setting up mutation observer', observer); - } - }, - - attachEvents: function(selector, event) { - var - $element = $(selector) - ; - event = $.isFunction(module[event]) - ? module[event] - : module.toggle - ; - if($element.length > 0) { - module.debug('Attaching checkbox events to element', selector, event); - $element - .on('click' + eventNamespace, event) - ; - } - else { - module.error(error.notFound); - } - }, - - preventDefaultOnInputTarget: function() { - if(typeof event !== 'undefined' && event !== null && $(event.target).is(selector.input)) { - module.verbose('Preventing default check action after manual check action'); - event.preventDefault(); - } - }, - - event: { - change: function(event) { - if( !module.should.ignoreCallbacks() ) { - settings.onChange.call(input); - } - }, - click: function(event) { - var - $target = $(event.target) - ; - if( $target.is(selector.input) ) { - module.verbose('Using default check action on initialized checkbox'); - return; - } - if( $target.is(selector.link) ) { - module.debug('Clicking link inside checkbox, skipping toggle'); - return; - } - module.toggle(); - $input.focus(); - event.preventDefault(); - }, - keydown: function(event) { - var - key = event.which, - keyCode = { - enter : 13, - space : 32, - escape : 27, - left : 37, - up : 38, - right : 39, - down : 40 - } - ; - - var r = module.get.radios(), - rIndex = r.index($module), - rLen = r.length, - checkIndex = false; - - if(key == keyCode.left || key == keyCode.up) { - checkIndex = (rIndex === 0 ? rLen : rIndex) - 1; - } else if(key == keyCode.right || key == keyCode.down) { - checkIndex = rIndex === rLen-1 ? 0 : rIndex+1; - } - - if (!module.should.ignoreCallbacks() && checkIndex !== false) { - if(settings.beforeUnchecked.apply(input)===false) { - module.verbose('Option not allowed to be unchecked, cancelling key navigation'); - return false; - } - if (settings.beforeChecked.apply($(r[checkIndex]).children(selector.input)[0])===false) { - module.verbose('Next option should not allow check, cancelling key navigation'); - return false; - } - } - - if(key == keyCode.escape) { - module.verbose('Escape key pressed blurring field'); - $input.blur(); - shortcutPressed = true; - } - else if(!event.ctrlKey && ( key == keyCode.space || (key == keyCode.enter && settings.enableEnterKey)) ) { - module.verbose('Enter/space key pressed, toggling checkbox'); - module.toggle(); - shortcutPressed = true; - } - else { - shortcutPressed = false; - } - }, - keyup: function(event) { - if(shortcutPressed) { - event.preventDefault(); - } - } - }, - - check: function() { - if( !module.should.allowCheck() ) { - return; - } - module.debug('Checking checkbox', $input); - module.set.checked(); - if( !module.should.ignoreCallbacks() ) { - settings.onChecked.call(input); - module.trigger.change(); - } - module.preventDefaultOnInputTarget(); - }, - - uncheck: function() { - if( !module.should.allowUncheck() ) { - return; - } - module.debug('Unchecking checkbox'); - module.set.unchecked(); - if( !module.should.ignoreCallbacks() ) { - settings.onUnchecked.call(input); - module.trigger.change(); - } - module.preventDefaultOnInputTarget(); - }, - - indeterminate: function() { - if( module.should.allowIndeterminate() ) { - module.debug('Checkbox is already indeterminate'); - return; - } - module.debug('Making checkbox indeterminate'); - module.set.indeterminate(); - if( !module.should.ignoreCallbacks() ) { - settings.onIndeterminate.call(input); - module.trigger.change(); - } - }, - - determinate: function() { - if( module.should.allowDeterminate() ) { - module.debug('Checkbox is already determinate'); - return; - } - module.debug('Making checkbox determinate'); - module.set.determinate(); - if( !module.should.ignoreCallbacks() ) { - settings.onDeterminate.call(input); - module.trigger.change(); - } - }, - - enable: function() { - if( module.is.enabled() ) { - module.debug('Checkbox is already enabled'); - return; - } - module.debug('Enabling checkbox'); - module.set.enabled(); - if( !module.should.ignoreCallbacks() ) { - settings.onEnable.call(input); - // preserve legacy callbacks - settings.onEnabled.call(input); - module.trigger.change(); - } - }, - - disable: function() { - if( module.is.disabled() ) { - module.debug('Checkbox is already disabled'); - return; - } - module.debug('Disabling checkbox'); - module.set.disabled(); - if( !module.should.ignoreCallbacks() ) { - settings.onDisable.call(input); - // preserve legacy callbacks - settings.onDisabled.call(input); - module.trigger.change(); - } - }, - - get: { - radios: function() { - var - name = module.get.name() - ; - return $('input[name="' + name + '"]').closest(selector.checkbox); - }, - otherRadios: function() { - return module.get.radios().not($module); - }, - name: function() { - return $input.attr('name'); - } - }, - - is: { - initialLoad: function() { - return initialLoad; - }, - radio: function() { - return ($input.hasClass(className.radio) || $input.attr('type') == 'radio'); - }, - indeterminate: function() { - return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate'); - }, - checked: function() { - return $input.prop('checked') !== undefined && $input.prop('checked'); - }, - disabled: function() { - return $input.prop('disabled') !== undefined && $input.prop('disabled'); - }, - enabled: function() { - return !module.is.disabled(); - }, - determinate: function() { - return !module.is.indeterminate(); - }, - unchecked: function() { - return !module.is.checked(); - } - }, - - should: { - allowCheck: function() { - if(module.is.determinate() && module.is.checked() && !module.is.initialLoad() ) { - module.debug('Should not allow check, checkbox is already checked'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeChecked.apply(input) === false) { - module.debug('Should not allow check, beforeChecked cancelled'); - return false; - } - return true; - }, - allowUncheck: function() { - if(module.is.determinate() && module.is.unchecked() && !module.is.initialLoad() ) { - module.debug('Should not allow uncheck, checkbox is already unchecked'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeUnchecked.apply(input) === false) { - module.debug('Should not allow uncheck, beforeUnchecked cancelled'); - return false; - } - return true; - }, - allowIndeterminate: function() { - if(module.is.indeterminate() && !module.is.initialLoad() ) { - module.debug('Should not allow indeterminate, checkbox is already indeterminate'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeIndeterminate.apply(input) === false) { - module.debug('Should not allow indeterminate, beforeIndeterminate cancelled'); - return false; - } - return true; - }, - allowDeterminate: function() { - if(module.is.determinate() && !module.is.initialLoad() ) { - module.debug('Should not allow determinate, checkbox is already determinate'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeDeterminate.apply(input) === false) { - module.debug('Should not allow determinate, beforeDeterminate cancelled'); - return false; - } - return true; - }, - ignoreCallbacks: function() { - return (initialLoad && !settings.fireOnInit); - } - }, - - can: { - change: function() { - return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') ); - }, - uncheck: function() { - return (typeof settings.uncheckable === 'boolean') - ? settings.uncheckable - : !module.is.radio() - ; - } - }, - - set: { - initialLoad: function() { - initialLoad = true; - }, - checked: function() { - module.verbose('Setting class to checked'); - $module - .removeClass(className.indeterminate) - .addClass(className.checked) - ; - if( module.is.radio() ) { - module.uncheckOthers(); - } - if(!module.is.indeterminate() && module.is.checked()) { - module.debug('Input is already checked, skipping input property change'); - return; - } - module.verbose('Setting state to checked', input); - $input - .prop('indeterminate', false) - .prop('checked', true) - ; - }, - unchecked: function() { - module.verbose('Removing checked class'); - $module - .removeClass(className.indeterminate) - .removeClass(className.checked) - ; - if(!module.is.indeterminate() && module.is.unchecked() ) { - module.debug('Input is already unchecked'); - return; - } - module.debug('Setting state to unchecked'); - $input - .prop('indeterminate', false) - .prop('checked', false) - ; - }, - indeterminate: function() { - module.verbose('Setting class to indeterminate'); - $module - .addClass(className.indeterminate) - ; - if( module.is.indeterminate() ) { - module.debug('Input is already indeterminate, skipping input property change'); - return; - } - module.debug('Setting state to indeterminate'); - $input - .prop('indeterminate', true) - ; - }, - determinate: function() { - module.verbose('Removing indeterminate class'); - $module - .removeClass(className.indeterminate) - ; - if( module.is.determinate() ) { - module.debug('Input is already determinate, skipping input property change'); - return; - } - module.debug('Setting state to determinate'); - $input - .prop('indeterminate', false) - ; - }, - disabled: function() { - module.verbose('Setting class to disabled'); - $module - .addClass(className.disabled) - ; - if( module.is.disabled() ) { - module.debug('Input is already disabled, skipping input property change'); - return; - } - module.debug('Setting state to disabled'); - $input - .prop('disabled', 'disabled') - ; - }, - enabled: function() { - module.verbose('Removing disabled class'); - $module.removeClass(className.disabled); - if( module.is.enabled() ) { - module.debug('Input is already enabled, skipping input property change'); - return; - } - module.debug('Setting state to enabled'); - $input - .prop('disabled', false) - ; - }, - tabbable: function() { - module.verbose('Adding tabindex to checkbox'); - if( $input.attr('tabindex') === undefined) { - $input.attr('tabindex', 0); - } - } - }, - - remove: { - initialLoad: function() { - initialLoad = false; - } - }, - - trigger: { - change: function() { - var - inputElement = $input[0] - ; - if(inputElement) { - var events = document.createEvent('HTMLEvents'); - module.verbose('Triggering native change event'); - events.initEvent('change', true, false); - inputElement.dispatchEvent(events); - } - } - }, - - - create: { - label: function() { - if($input.prevAll(selector.label).length > 0) { - $input.prev(selector.label).detach().insertAfter($input); - module.debug('Moving existing label', $label); - } - else if( !module.has.label() ) { - $label = $('<label>').insertAfter($input); - module.debug('Creating label', $label); - } - } - }, - - has: { - label: function() { - return ($label.length > 0); - } - }, - - bind: { - events: function() { - module.verbose('Attaching checkbox events'); - $module - .on('click' + eventNamespace, module.event.click) - .on('change' + eventNamespace, module.event.change) - .on('keydown' + eventNamespace, selector.input, module.event.keydown) - .on('keyup' + eventNamespace, selector.input, module.event.keyup) - ; - } - }, - - unbind: { - events: function() { - module.debug('Removing events'); - $module - .off(eventNamespace) - ; - } - }, - - uncheckOthers: function() { - var - $radios = module.get.otherRadios() - ; - module.debug('Unchecking other radios', $radios); - $radios.removeClass(className.checked); - }, - - toggle: function() { - if( !module.can.change() ) { - if(!module.is.radio()) { - module.debug('Checkbox is read-only or disabled, ignoring toggle'); - } - return; - } - if( module.is.indeterminate() || module.is.unchecked() ) { - module.debug('Currently unchecked'); - module.check(); - } - else if( module.is.checked() && module.can.uncheck() ) { - module.debug('Currently checked'); - module.uncheck(); - } - }, - setting: function(name, value) { - module.debug('Changing setting', name, value); - if( $.isPlainObject(name) ) { - $.extend(true, settings, name); - } - else if(value !== undefined) { - if($.isPlainObject(settings[name])) { - $.extend(true, settings[name], value); - } - else { - settings[name] = value; - } - } - else { - return settings[name]; - } - }, - internal: function(name, value) { - if( $.isPlainObject(name) ) { - $.extend(true, module, name); - } - else if(value !== undefined) { - module[name] = value; - } - else { - return module[name]; - } - }, - debug: function() { - if(!settings.silent && settings.debug) { - if(settings.performance) { - module.performance.log(arguments); - } - else { - module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); - module.debug.apply(console, arguments); - } - } - }, - verbose: function() { - if(!settings.silent && settings.verbose && settings.debug) { - if(settings.performance) { - module.performance.log(arguments); - } - else { - module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); - module.verbose.apply(console, arguments); - } - } - }, - error: function() { - if(!settings.silent) { - module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); - module.error.apply(console, arguments); - } - }, - performance: { - log: function(message) { - var - currentTime, - executionTime, - previousTime - ; - if(settings.performance) { - currentTime = new Date().getTime(); - previousTime = time || currentTime; - executionTime = currentTime - previousTime; - time = currentTime; - performance.push({ - 'Name' : message[0], - 'Arguments' : [].slice.call(message, 1) || '', - 'Element' : element, - 'Execution Time' : executionTime - }); - } - clearTimeout(module.performance.timer); - module.performance.timer = setTimeout(module.performance.display, 500); - }, - display: function() { - var - title = settings.name + ':', - totalTime = 0 - ; - time = false; - clearTimeout(module.performance.timer); - $.each(performance, function(index, data) { - totalTime += data['Execution Time']; - }); - title += ' ' + totalTime + 'ms'; - if(moduleSelector) { - title += ' \'' + moduleSelector + '\''; - } - if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { - console.groupCollapsed(title); - if(console.table) { - console.table(performance); - } - else { - $.each(performance, function(index, data) { - console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); - }); - } - console.groupEnd(); - } - performance = []; - } - }, - invoke: function(query, passedArguments, context) { - var - object = instance, - maxDepth, - found, - response - ; - passedArguments = passedArguments || queryArguments; - context = element || context; - if(typeof query == 'string' && object !== undefined) { - query = query.split(/[\. ]/); - maxDepth = query.length - 1; - $.each(query, function(depth, value) { - var camelCaseValue = (depth != maxDepth) - ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) - : query - ; - if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { - object = object[camelCaseValue]; - } - else if( object[camelCaseValue] !== undefined ) { - found = object[camelCaseValue]; - return false; - } - else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { - object = object[value]; - } - else if( object[value] !== undefined ) { - found = object[value]; - return false; - } - else { - module.error(error.method, query); - return false; - } - }); - } - if ( $.isFunction( found ) ) { - response = found.apply(context, passedArguments); - } - else if(found !== undefined) { - response = found; - } - if(Array.isArray(returnedValue)) { - returnedValue.push(response); - } - else if(returnedValue !== undefined) { - returnedValue = [returnedValue, response]; - } - else if(response !== undefined) { - returnedValue = response; - } - return found; - } - }; - - if(methodInvoked) { - if(instance === undefined) { - module.initialize(); - } - module.invoke(query); - } - else { - if(instance !== undefined) { - instance.invoke('destroy'); - } - module.initialize(); - } - }) - ; - - return (returnedValue !== undefined) - ? returnedValue - : this - ; -}; - -$.fn.checkbox.settings = { - - name : 'Checkbox', - namespace : 'checkbox', - - silent : false, - debug : false, - verbose : true, - performance : true, - - // delegated event context - uncheckable : 'auto', - fireOnInit : false, - enableEnterKey : true, - - onChange : function(){}, - - beforeChecked : function(){}, - beforeUnchecked : function(){}, - beforeDeterminate : function(){}, - beforeIndeterminate : function(){}, - - onChecked : function(){}, - onUnchecked : function(){}, - - onDeterminate : function() {}, - onIndeterminate : function() {}, - - onEnable : function(){}, - onDisable : function(){}, - - // preserve misspelled callbacks (will be removed in 3.0) - onEnabled : function(){}, - onDisabled : function(){}, - - className : { - checked : 'checked', - indeterminate : 'indeterminate', - disabled : 'disabled', - hidden : 'hidden', - radio : 'radio', - readOnly : 'read-only' - }, - - error : { - method : 'The method you called is not defined' - }, - - selector : { - checkbox : '.ui.checkbox', - label : 'label, .box', - input : 'input[type="checkbox"], input[type="radio"]', - link : 'a[href]' - } - -}; - })( jQuery, window, document ); /*! @@ -11864,500 +10987,6 @@ $.fn.search.settings = { } }; -})( jQuery, window, document ); - -/*! - * # Fomantic-UI - Site - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -;(function ($, window, document, undefined) { - -$.isFunction = $.isFunction || function(obj) { - return typeof obj === "function" && typeof obj.nodeType !== "number"; -}; - -$.site = $.fn.site = function(parameters) { - var - time = new Date().getTime(), - performance = [], - - query = arguments[0], - methodInvoked = (typeof query == 'string'), - queryArguments = [].slice.call(arguments, 1), - - settings = ( $.isPlainObject(parameters) ) - ? $.extend(true, {}, $.site.settings, parameters) - : $.extend({}, $.site.settings), - - namespace = settings.namespace, - error = settings.error, - - moduleNamespace = 'module-' + namespace, - - $document = $(document), - $module = $document, - element = this, - instance = $module.data(moduleNamespace), - - module, - returnedValue - ; - module = { - - initialize: function() { - module.instantiate(); - }, - - instantiate: function() { - module.verbose('Storing instance of site', module); - instance = module; - $module - .data(moduleNamespace, module) - ; - }, - - normalize: function() { - module.fix.console(); - module.fix.requestAnimationFrame(); - }, - - fix: { - console: function() { - module.debug('Normalizing window.console'); - if (console === undefined || console.log === undefined) { - module.verbose('Console not available, normalizing events'); - module.disable.console(); - } - if (typeof console.group == 'undefined' || typeof console.groupEnd == 'undefined' || typeof console.groupCollapsed == 'undefined') { - module.verbose('Console group not available, normalizing events'); - window.console.group = function() {}; - window.console.groupEnd = function() {}; - window.console.groupCollapsed = function() {}; - } - if (typeof console.markTimeline == 'undefined') { - module.verbose('Mark timeline not available, normalizing events'); - window.console.markTimeline = function() {}; - } - }, - consoleClear: function() { - module.debug('Disabling programmatic console clearing'); - window.console.clear = function() {}; - }, - requestAnimationFrame: function() { - module.debug('Normalizing requestAnimationFrame'); - if(window.requestAnimationFrame === undefined) { - module.debug('RequestAnimationFrame not available, normalizing event'); - window.requestAnimationFrame = window.requestAnimationFrame - || window.mozRequestAnimationFrame - || window.webkitRequestAnimationFrame - || window.msRequestAnimationFrame - || function(callback) { setTimeout(callback, 0); } - ; - } - } - }, - - moduleExists: function(name) { - return ($.fn[name] !== undefined && $.fn[name].settings !== undefined); - }, - - enabled: { - modules: function(modules) { - var - enabledModules = [] - ; - modules = modules || settings.modules; - $.each(modules, function(index, name) { - if(module.moduleExists(name)) { - enabledModules.push(name); - } - }); - return enabledModules; - } - }, - - disabled: { - modules: function(modules) { - var - disabledModules = [] - ; - modules = modules || settings.modules; - $.each(modules, function(index, name) { - if(!module.moduleExists(name)) { - disabledModules.push(name); - } - }); - return disabledModules; - } - }, - - change: { - setting: function(setting, value, modules, modifyExisting) { - modules = (typeof modules === 'string') - ? (modules === 'all') - ? settings.modules - : [modules] - : modules || settings.modules - ; - modifyExisting = (modifyExisting !== undefined) - ? modifyExisting - : true - ; - $.each(modules, function(index, name) { - var - namespace = (module.moduleExists(name)) - ? $.fn[name].settings.namespace || false - : true, - $existingModules - ; - if(module.moduleExists(name)) { - module.verbose('Changing default setting', setting, value, name); - $.fn[name].settings[setting] = value; - if(modifyExisting && namespace) { - $existingModules = $(':data(module-' + namespace + ')'); - if($existingModules.length > 0) { - module.verbose('Modifying existing settings', $existingModules); - $existingModules[name]('setting', setting, value); - } - } - } - }); - }, - settings: function(newSettings, modules, modifyExisting) { - modules = (typeof modules === 'string') - ? [modules] - : modules || settings.modules - ; - modifyExisting = (modifyExisting !== undefined) - ? modifyExisting - : true - ; - $.each(modules, function(index, name) { - var - $existingModules - ; - if(module.moduleExists(name)) { - module.verbose('Changing default setting', newSettings, name); - $.extend(true, $.fn[name].settings, newSettings); - if(modifyExisting && namespace) { - $existingModules = $(':data(module-' + namespace + ')'); - if($existingModules.length > 0) { - module.verbose('Modifying existing settings', $existingModules); - $existingModules[name]('setting', newSettings); - } - } - } - }); - } - }, - - enable: { - console: function() { - module.console(true); - }, - debug: function(modules, modifyExisting) { - modules = modules || settings.modules; - module.debug('Enabling debug for modules', modules); - module.change.setting('debug', true, modules, modifyExisting); - }, - verbose: function(modules, modifyExisting) { - modules = modules || settings.modules; - module.debug('Enabling verbose debug for modules', modules); - module.change.setting('verbose', true, modules, modifyExisting); - } - }, - disable: { - console: function() { - module.console(false); - }, - debug: function(modules, modifyExisting) { - modules = modules || settings.modules; - module.debug('Disabling debug for modules', modules); - module.change.setting('debug', false, modules, modifyExisting); - }, - verbose: function(modules, modifyExisting) { - modules = modules || settings.modules; - module.debug('Disabling verbose debug for modules', modules); - module.change.setting('verbose', false, modules, modifyExisting); - } - }, - - console: function(enable) { - if(enable) { - if(instance.cache.console === undefined) { - module.error(error.console); - return; - } - module.debug('Restoring console function'); - window.console = instance.cache.console; - } - else { - module.debug('Disabling console function'); - instance.cache.console = window.console; - window.console = { - clear : function(){}, - error : function(){}, - group : function(){}, - groupCollapsed : function(){}, - groupEnd : function(){}, - info : function(){}, - log : function(){}, - markTimeline : function(){}, - warn : function(){} - }; - } - }, - - destroy: function() { - module.verbose('Destroying previous site for', $module); - $module - .removeData(moduleNamespace) - ; - }, - - cache: {}, - - setting: function(name, value) { - if( $.isPlainObject(name) ) { - $.extend(true, settings, name); - } - else if(value !== undefined) { - settings[name] = value; - } - else { - return settings[name]; - } - }, - internal: function(name, value) { - if( $.isPlainObject(name) ) { - $.extend(true, module, name); - } - else if(value !== undefined) { - module[name] = value; - } - else { - return module[name]; - } - }, - debug: function() { - if(settings.debug) { - if(settings.performance) { - module.performance.log(arguments); - } - else { - module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); - module.debug.apply(console, arguments); - } - } - }, - verbose: function() { - if(settings.verbose && settings.debug) { - if(settings.performance) { - module.performance.log(arguments); - } - else { - module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); - module.verbose.apply(console, arguments); - } - } - }, - error: function() { - module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); - module.error.apply(console, arguments); - }, - performance: { - log: function(message) { - var - currentTime, - executionTime, - previousTime - ; - if(settings.performance) { - currentTime = new Date().getTime(); - previousTime = time || currentTime; - executionTime = currentTime - previousTime; - time = currentTime; - performance.push({ - 'Element' : element, - 'Name' : message[0], - 'Arguments' : [].slice.call(message, 1) || '', - 'Execution Time' : executionTime - }); - } - clearTimeout(module.performance.timer); - module.performance.timer = setTimeout(module.performance.display, 500); - }, - display: function() { - var - title = settings.name + ':', - totalTime = 0 - ; - time = false; - clearTimeout(module.performance.timer); - $.each(performance, function(index, data) { - totalTime += data['Execution Time']; - }); - title += ' ' + totalTime + 'ms'; - if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { - console.groupCollapsed(title); - if(console.table) { - console.table(performance); - } - else { - $.each(performance, function(index, data) { - console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); - }); - } - console.groupEnd(); - } - performance = []; - } - }, - invoke: function(query, passedArguments, context) { - var - object = instance, - maxDepth, - found, - response - ; - passedArguments = passedArguments || queryArguments; - context = element || context; - if(typeof query == 'string' && object !== undefined) { - query = query.split(/[\. ]/); - maxDepth = query.length - 1; - $.each(query, function(depth, value) { - var camelCaseValue = (depth != maxDepth) - ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) - : query - ; - if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { - object = object[camelCaseValue]; - } - else if( object[camelCaseValue] !== undefined ) { - found = object[camelCaseValue]; - return false; - } - else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { - object = object[value]; - } - else if( object[value] !== undefined ) { - found = object[value]; - return false; - } - else { - module.error(error.method, query); - return false; - } - }); - } - if ( $.isFunction( found ) ) { - response = found.apply(context, passedArguments); - } - else if(found !== undefined) { - response = found; - } - if(Array.isArray(returnedValue)) { - returnedValue.push(response); - } - else if(returnedValue !== undefined) { - returnedValue = [returnedValue, response]; - } - else if(response !== undefined) { - returnedValue = response; - } - return found; - } - }; - - if(methodInvoked) { - if(instance === undefined) { - module.initialize(); - } - module.invoke(query); - } - else { - if(instance !== undefined) { - module.destroy(); - } - module.initialize(); - } - return (returnedValue !== undefined) - ? returnedValue - : this - ; -}; - -$.site.settings = { - - name : 'Site', - namespace : 'site', - - error : { - console : 'Console cannot be restored, most likely it was overwritten outside of module', - method : 'The method you called is not defined.' - }, - - debug : false, - verbose : false, - performance : true, - - modules: [ - 'accordion', - 'api', - 'calendar', - 'checkbox', - 'dimmer', - 'dropdown', - 'embed', - 'form', - 'modal', - 'nag', - 'popup', - 'slider', - 'rating', - 'shape', - 'sidebar', - 'state', - 'sticky', - 'tab', - 'toast', - 'transition', - 'visibility', - 'visit' - ], - - siteNamespace : 'site', - namespaceStub : { - cache : {}, - config : {}, - sections : {}, - section : {}, - utilities : {} - } - -}; - -// allows for selection of elements with data attributes -$.extend($.expr[ ":" ], { - data: ($.expr.createPseudo) - ? $.expr.createPseudo(function(dataName) { - return function(elem) { - return !!$.data(elem, dataName); - }; - }) - : function(elem, i, match) { - // support: jQuery < 1.8 - return !!$.data(elem, match[ 3 ]); - } -}); - - })( jQuery, window, document ); /*! diff --git a/web_src/fomantic/package-lock.json b/web_src/fomantic/package-lock.json index 8283122eb3..4000d10da7 100644 --- a/web_src/fomantic/package-lock.json +++ b/web_src/fomantic/package-lock.json @@ -19,6 +19,100 @@ "findup": "bin/findup.js" } }, + "node_modules/@choojs/findup/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "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==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "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==" + }, + "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==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@octokit/auth-token": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", @@ -28,35 +122,97 @@ } }, "node_modules/@octokit/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", - "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.0.1.tgz", + "integrity": "sha512-MIpPQXu8Y8GjHwXM81JLveiV+DHJZtLMcB5nKekBGOl3iAtk0HT3i12Xl8Biybu+bCS1+k4qbuKEq5d0RxNRnQ==", "peer": true, "dependencies": { - "@octokit/auth-token": "^2.4.4", - "@octokit/graphql": "^4.5.8", - "@octokit/request": "^5.6.3", - "@octokit/request-error": "^2.0.5", - "@octokit/types": "^6.0.3", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^12.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/auth-token": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.0.1.tgz", + "integrity": "sha512-RTmWsLfig8SBoiSdgvCht4BXl1CHU89Co5xiQ5JF19my/sIRDFCQ1RPrmK0exgqUZuNm39C/bV8+/83+MJEjGg==", + "peer": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/endpoint": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.0.0.tgz", + "integrity": "sha512-emBcNDxBdC1y3+knJonS5zhUB/CG6TihubxM2U1/pG/Z1y3a4oV0Gzz3lmkCvWWQI6h3tqBAX9MgCBFp+M68Jw==", + "peer": true, + "dependencies": { + "@octokit/types": "^12.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "peer": true + }, + "node_modules/@octokit/core/node_modules/@octokit/request": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.0.1.tgz", + "integrity": "sha512-kL+cAcbSl3dctYLuJmLfx6Iku2MXXy0jszhaEIjQNaCp4zjHXrhVAHeuaRdNvJjW9qjl3u1MJ72+OuBP0YW/pg==", + "peer": true, + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/core/node_modules/@octokit/request-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", - "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.0.2.tgz", + "integrity": "sha512-WtRVpoHcNXs84+s9s/wqfHaxM68NGMg8Av7h59B50OVO0PwwMx+2GgQ/OliUd0iQBSNWgR6N8afi/KjSHbXHWw==", "peer": true, "dependencies": { - "@octokit/types": "^6.0.3", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" } }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "peer": true, + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/core/node_modules/before-after-hook": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "peer": true + }, "node_modules/@octokit/core/node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", "peer": true }, "node_modules/@octokit/endpoint": { @@ -70,31 +226,89 @@ } }, "node_modules/@octokit/endpoint/node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" }, "node_modules/@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.0.1.tgz", + "integrity": "sha512-lLDb6LhC1gBj2CxEDa5Xk10+H/boonhs+3Mi6jpRyetskDKNHe6crMeKmUE2efoLofMP8ruannLlCUgpTFmVzQ==", "peer": true, "dependencies": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^9.0.0", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.0.0.tgz", + "integrity": "sha512-emBcNDxBdC1y3+knJonS5zhUB/CG6TihubxM2U1/pG/Z1y3a4oV0Gzz3lmkCvWWQI6h3tqBAX9MgCBFp+M68Jw==", + "peer": true, + "dependencies": { + "@octokit/types": "^12.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "peer": true + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.0.1.tgz", + "integrity": "sha512-kL+cAcbSl3dctYLuJmLfx6Iku2MXXy0jszhaEIjQNaCp4zjHXrhVAHeuaRdNvJjW9qjl3u1MJ72+OuBP0YW/pg==", + "peer": true, + "dependencies": { + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.0.2.tgz", + "integrity": "sha512-WtRVpoHcNXs84+s9s/wqfHaxM68NGMg8Av7h59B50OVO0PwwMx+2GgQ/OliUd0iQBSNWgR6N8afi/KjSHbXHWw==", + "peer": true, + "dependencies": { + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "peer": true, + "dependencies": { + "@octokit/openapi-types": "^20.0.0" } }, "node_modules/@octokit/graphql/node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", "peer": true }, "node_modules/@octokit/openapi-types": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz", - "integrity": "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "1.1.2", @@ -179,9 +393,9 @@ } }, "node_modules/@octokit/request/node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" }, "node_modules/@octokit/rest": { "version": "16.43.2", @@ -207,11 +421,25 @@ } }, "node_modules/@octokit/types": { - "version": "6.34.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz", - "integrity": "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", "dependencies": { - "@octokit/openapi-types": "^11.2.0" + "@octokit/openapi-types": "^12.11.0" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" } }, "node_modules/@types/expect": { @@ -220,23 +448,29 @@ "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==" }, "node_modules/@types/node": { - "version": "14.18.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", - "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==" + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/vinyl": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz", - "integrity": "sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.11.tgz", + "integrity": "sha512-vPXzCLmRp74e9LsP8oltnWKTH+jBwt86WgRUb4Pc9Lf3pkMVGyvIo2gm9bODeGfCay2DBB/hAWDuvf07JcK4rw==", "dependencies": { "@types/expect": "^1.20.4", "@types/node": "*" } }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/accord": { "version": "0.29.0", @@ -590,9 +824,15 @@ } }, "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] }, "node_modules/async-settle": { "version": "1.0.0", @@ -703,9 +943,9 @@ } }, "node_modules/before-after-hook": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", - "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, "node_modules/better-console": { "version": "1.0.1", @@ -735,6 +975,15 @@ "url": "https://bevry.me/fund" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -765,9 +1014,9 @@ } }, "node_modules/browserslist": { - "version": "4.20.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz", - "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -776,14 +1025,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001349", - "electron-to-chromium": "^1.4.147", - "escalade": "^3.1.1", - "node-releases": "^2.0.5", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -792,22 +1044,20 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/browserslist/node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, "node_modules/btoa-lite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" }, "node_modules/buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", "engines": { - "node": ">=0.4.0" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/buffer-from": { @@ -835,12 +1085,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -855,9 +1111,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001352", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz", - "integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA==", + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", "funding": [ { "type": "opencollective", @@ -866,6 +1122,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -948,61 +1208,16 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/clean-css": { @@ -1171,14 +1386,20 @@ } }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } }, "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -1217,12 +1438,9 @@ } }, "node_modules/convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dependencies": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/copy-anything": { "version": "2.0.6", @@ -1284,12 +1502,15 @@ } }, "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/dateformat": { @@ -1317,9 +1538,9 @@ } }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "engines": { "node": ">=0.10" } @@ -1336,9 +1557,9 @@ } }, "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "engines": { "node": ">=0.10.0" } @@ -1362,11 +1583,28 @@ "node": ">= 0.10" } }, - "node_modules/define-properties": { + "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { + "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": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -1494,24 +1732,73 @@ "node": ">=0.10.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/editorconfig": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", "dependencies": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "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" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/electron-to-chromium": { - "version": "1.4.151", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.151.tgz", - "integrity": "sha512-XaG2LpZi9fdiWYOqJh0dJy4SlVywCvpgYXhzOlZTp4JqSKqxn5URqOjbm9OMYB3aInA2GuHQiem1QUOc1yT0Pw==" + "version": "1.4.690", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz", + "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==" + }, + "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/end-of-stream": { "version": "1.4.4", @@ -1521,6 +1808,18 @@ "once": "^1.4.0" } }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1529,14 +1828,34 @@ "is-arrayish": "^0.2.1" } }, + "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==", + "dependencies": { + "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==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es5-ext": { - "version": "0.10.61", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.61.tgz", - "integrity": "sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -1554,12 +1873,15 @@ } }, "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/es6-weak-map": { @@ -1574,9 +1896,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" } @@ -1589,6 +1911,29 @@ "node": ">=0.8.0" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -1634,61 +1979,16 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/expand-tilde": { @@ -1703,18 +2003,13 @@ } }, "node_modules/ext": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", - "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", "dependencies": { - "type": "^2.5.0" + "type": "^2.7.2" } }, - "node_modules/ext/node_modules/type": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz", - "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1803,6 +2098,12 @@ "node": ">=4" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "node_modules/fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1968,6 +2269,86 @@ "node": ">=0.10.0" } }, + "node_modules/foreground-child": { + "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==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/fork-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", @@ -2010,10 +2391,31 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-caller-file": { "version": "1.0.3", @@ -2033,13 +2435,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "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" @@ -2216,10 +2623,21 @@ "node": ">= 0.10" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/growly": { "version": "1.3.0", @@ -2889,21 +3307,32 @@ } }, "node_modules/gulp-json-editor": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.5.6.tgz", - "integrity": "sha512-66Xr6Q6m4mUNd0OOHflMB/RHgFNnLjlHgizOzUcx9CyMRymVZEM+/SpZcCDlvThBdXtQwXpdvtSepxVY/V6nQA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.6.0.tgz", + "integrity": "sha512-Ni0ZUpNrhesHiTlHQth/Nv1rXCn0LUicEvzA5XuGy186C4PVeNoRjfuAIQrbmt3scKv8dgGbCs0hd77ScTw7hA==", "dependencies": { - "deepmerge": "^4.2.2", - "detect-indent": "^6.0.0", - "js-beautify": "^1.13.13", - "plugin-error": "^1.0.1", + "deepmerge": "^4.3.1", + "detect-indent": "^6.1.0", + "js-beautify": "^1.14.11", + "plugin-error": "^2.0.1", "through2": "^4.0.2" } }, + "node_modules/gulp-json-editor/node_modules/plugin-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-2.0.1.tgz", + "integrity": "sha512-zMakqvIDyY40xHOvzXka0kUvf40nYIuwRE8dWhti2WtjQZ31xAgBZBhxsK7vK3QbRXS1Xms/LO7B5cuAsfB2Gg==", + "dependencies": { + "ansi-colors": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/gulp-json-editor/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3228,11 +3657,11 @@ } }, "node_modules/gulp-replace": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.3.tgz", - "integrity": "sha512-HcPHpWY4XdF8zxYkDODHnG2+7a3nD/Y8Mfu3aBgMiCFDW3X2GiOKXllsAmILcxe3KZT2BXoN18WrpEFm48KfLQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.1.4.tgz", + "integrity": "sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==", "dependencies": { - "@types/node": "^14.14.41", + "@types/node": "*", "@types/vinyl": "^2.0.4", "istextorbinary": "^3.0.0", "replacestream": "^4.0.3", @@ -3340,9 +3769,9 @@ } }, "node_modules/gulp-uglify/node_modules/uglify-js": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.0.tgz", - "integrity": "sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==", + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -3445,7 +3874,7 @@ "node_modules/gulp-util/node_modules/vinyl": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "integrity": "sha512-P5zdf3WB9uzr7IFoVQ2wZTmUwHL8cMZWJGzLBNCHNZ3NB6HTMsYABtt7z8tAGIINLXyAob9B9a1yzVGMFOYKEA==", "dependencies": { "clone": "^1.0.0", "clone-stats": "^0.0.1", @@ -3466,17 +3895,6 @@ "node": ">= 0.10" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -3508,11 +3926,22 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3565,6 +3994,17 @@ "node": ">=0.10.0" } }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -3592,6 +4032,18 @@ "node": ">=0.10.0" } }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/import-regex/-/import-regex-1.1.0.tgz", @@ -3754,22 +4206,14 @@ } }, "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", "dependencies": { - "kind-of": "^6.0.0" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/is-arrayish": { @@ -3794,54 +4238,37 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", "dependencies": { - "kind-of": "^6.0.0" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/is-extendable": { @@ -4060,20 +4487,38 @@ "url": "https://bevry.me/fund" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jquery": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", - "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "node_modules/js-beautify": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.3.tgz", - "integrity": "sha512-f1ra8PHtOEu/70EBnmiUlV8nJePS58y9qKjl4JHfYWlFH6bo7ogZBz//FAZp7jDuXtYnGYKymZPlrg2I/9Zo4g==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^0.15.3", - "glob": "^7.1.3", - "nopt": "^5.0.0" + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" }, "bin": { "css-beautify": "js/bin/css-beautify.js", @@ -4081,7 +4526,58 @@ "js-beautify": "js/bin/js-beautify.js" }, "engines": { - "node": ">=10" + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "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/js-beautify/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" } }, "node_modules/json-stable-stringify-without-jsonify": { @@ -4449,18 +4945,20 @@ } }, "node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/macos-release": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.0.tgz", - "integrity": "sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", "engines": { "node": ">=6" }, @@ -4468,6 +4966,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4633,6 +5153,18 @@ "node": ">=0.10.0" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -4653,9 +5185,20 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } }, "node_modules/mixin-deep": { "version": "1.3.2", @@ -4728,6 +5271,12 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==" }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", + "optional": true + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -4791,6 +5340,12 @@ "node": ">=0.10.0" } }, + "node_modules/native-request": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.1.0.tgz", + "integrity": "sha512-uZ5rQaeRn15XmpgE0xoPL8YWqcX90VtCFglYwAgkvKM5e8fog+vePLAhHxuuv/gRkrQxIeh5U3q9sMNUrENqWw==", + "optional": true + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -4802,9 +5357,9 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -4833,34 +5388,34 @@ } }, "node_modules/node-releases": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", - "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/node.extend": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", - "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.3.tgz", + "integrity": "sha512-xwADg/okH48PvBmRZyoX8i8GJaKuJ1CqlqotlZOhUio8egD1P5trJupHKBzcPjSF9ifK2gPcEICRBnkfPqQXZw==", "dependencies": { - "has": "^1.0.3", - "is": "^3.2.1" + "hasown": "^2.0.0", + "is": "^3.3.0" }, "engines": { "node": ">=0.4.0" } }, "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", "dependencies": { - "abbrev": "1" + "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/normalize-package-data": { @@ -4957,47 +5512,16 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/object-copy/node_modules/kind-of": { @@ -5031,13 +5555,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, "engines": { @@ -5303,6 +5827,29 @@ "node": ">=0.10.0" } }, + "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==", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", @@ -5462,10 +6009,11 @@ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "optional": true }, "node_modules/pump": { "version": "2.0.1", @@ -5512,9 +6060,9 @@ } }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5525,6 +6073,11 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -5708,11 +6261,11 @@ "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==" }, "node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dependencies": { - "is-core-module": "^2.8.1", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -5963,9 +6516,23 @@ } }, "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/safe-regex": { "version": "1.1.0", @@ -5981,9 +6548,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -6004,6 +6571,22 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -6053,11 +6636,6 @@ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" }, - "node_modules/sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==" - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -6138,61 +6716,16 @@ "node": ">=0.10.0" } }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/snapdragon/node_modules/source-map": { @@ -6239,18 +6772,18 @@ } }, "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + "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", @@ -6262,9 +6795,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", - "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==" + "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/split-string": { "version": "3.1.0", @@ -6352,61 +6885,16 @@ "node": ">=0.10.0" } }, - "node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/stream-exhaust": { @@ -6415,9 +6903,9 @@ "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==" }, "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, "node_modules/string_decoder": { "version": "1.1.1", @@ -6427,6 +6915,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -6439,6 +6932,47 @@ "node": ">=4" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", @@ -6482,6 +7016,26 @@ "node": ">=0.10.0" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", @@ -6667,7 +7221,7 @@ "node_modules/to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", "dependencies": { "is-absolute": "^1.0.0", "is-negated-glob": "^1.0.0" @@ -6679,7 +7233,7 @@ "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", "dependencies": { "kind-of": "^3.0.2" }, @@ -6715,7 +7269,7 @@ "node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dependencies": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -6761,7 +7315,7 @@ "node_modules/to-through": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", "dependencies": { "through2": "^2.0.3" }, @@ -6781,7 +7335,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/tslib": { "version": "1.14.1", @@ -6789,19 +7343,19 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", "dependencies": { "source-map": "~0.5.1", "yargs": "~3.10.0" @@ -6845,7 +7399,7 @@ "node_modules/uglify-js/node_modules/yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", "dependencies": { "camelcase": "^1.0.2", "cliui": "^2.1.0", @@ -6853,10 +7407,16 @@ "window-size": "0.1.0" } }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", + "optional": true + }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "engines": { "node": ">=0.10.0" } @@ -6884,11 +7444,16 @@ "node_modules/undertaker-registry": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "integrity": "sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw==", "engines": { "node": ">= 0.10" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -6923,7 +7488,7 @@ "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", "dependencies": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -6973,16 +7538,50 @@ "yarn": "*" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-browserslist-db/node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", "deprecated": "Please see https://github.com/lydell/urix#deprecated" }, "node_modules/url-regex": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz", - "integrity": "sha1-260eDJ4p4QXdCx8J9oYvf9tIJyQ=", + "integrity": "sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==", "dependencies": { "ip-regex": "^1.0.1" }, @@ -7001,7 +7600,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/v8flags": { "version": "3.2.0", @@ -7026,7 +7625,7 @@ "node_modules/value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", "engines": { "node": ">= 0.10" } @@ -7086,7 +7685,7 @@ "node_modules/vinyl-sourcemap": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", "dependencies": { "append-buffer": "^1.0.2", "convert-source-map": "^1.5.0", @@ -7114,7 +7713,7 @@ "node_modules/vinyl-sourcemaps-apply": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "integrity": "sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==", "dependencies": { "source-map": "^0.5.1" } @@ -7130,12 +7729,12 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -7144,7 +7743,7 @@ "node_modules/when": { "version": "3.7.8", "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", - "integrity": "sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I=" + "integrity": "sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw==" }, "node_modules/which": { "version": "1.3.1", @@ -7160,12 +7759,12 @@ "node_modules/which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==" }, "node_modules/window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", "engines": { "node": ">= 0.8.0" } @@ -7187,7 +7786,7 @@ "node_modules/wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", "engines": { "node": ">=0.4.0" } @@ -7195,7 +7794,7 @@ "node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dependencies": { "string-width": "^1.0.1", "strip-ansi": "^3.0.1" @@ -7204,6 +7803,93 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi-cjs": { + "name": "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==", + "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/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrap-ansi-cjs/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", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -7231,12 +7917,12 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/wrench-sui": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wrench-sui/-/wrench-sui-0.0.3.tgz", - "integrity": "sha1-1hoSAwwf2NZxs90VqmyeD83E4sg=", + "integrity": "sha512-Y6qzMpcMG9akKnIdUsKzEF/Ht0KQJBP8ETkZj3FcGe93NC71e940WZUP1y+j+hc8Ecx9TyX0GvAWC4yymA88yA==", "engines": { "node": ">=0.1.97" } @@ -7255,9 +7941,9 @@ "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" }, "node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yamljs": { "version": "0.3.0", @@ -7293,9 +7979,9 @@ } }, "node_modules/yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "engines": { "node": ">=12" } diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json index 43d0b412b3..5db57bc8d4 100644 --- a/web_src/fomantic/semantic.json +++ b/web_src/fomantic/semantic.json @@ -23,23 +23,12 @@ "components": [ "api", "button", - "checkbox", - "container", "dimmer", "dropdown", "form", - "grid", - "header", - "input", - "label", - "list", "menu", - "message", "modal", "search", - "segment", - "site", - "tab", - "table" + "tab" ] } diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js index f8d0c0cac0..3034478190 100644 --- a/web_src/js/bootstrap.js +++ b/web_src/js/bootstrap.js @@ -1,20 +1,29 @@ // DO NOT IMPORT window.config HERE! -// to make sure the error handler always works, we should never import `window.config`, because some user's custom template breaks it. +// to make sure the error handler always works, we should never import `window.config`, because +// some user's custom template breaks it. // This sets up the URL prefix used in webpack's chunk loading. // This file must be imported before any lazy-loading is being attempted. __webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`; +const filteredErrors = new Set([ + 'getModifierState is not a function', // https://github.com/microsoft/monaco-editor/issues/4325 +]); + export function showGlobalErrorMessage(msg) { const pageContent = document.querySelector('.page-content'); if (!pageContent) return; + for (const filteredError of filteredErrors) { + if (msg.includes(filteredError)) return; + } + // compact the message to a data attribute to avoid too many duplicated messages const msgCompact = msg.replace(/\W/g, '').trim(); let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); if (!msgDiv) { const el = document.createElement('div'); - el.innerHTML = `<div class="ui container negative message center aligned js-global-error" style="white-space: pre-line;"></div>`; + el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`; msgDiv = el.childNodes[0]; } // merge duplicated messages into "the message (count)" format @@ -26,20 +35,42 @@ export function showGlobalErrorMessage(msg) { } /** - * @param {ErrorEvent} e + * @param {ErrorEvent|PromiseRejectionEvent} event - Event + * @param {string} event.message - Only present on ErrorEvent + * @param {string} event.error - Only present on ErrorEvent + * @param {string} event.type - Only present on ErrorEvent + * @param {string} event.filename - Only present on ErrorEvent + * @param {number} event.lineno - Only present on ErrorEvent + * @param {number} event.colno - Only present on ErrorEvent + * @param {string} event.reason - Only present on PromiseRejectionEvent + * @param {number} event.promise - Only present on PromiseRejectionEvent */ -function processWindowErrorEvent(e) { - if (e.type === 'unhandledrejection') { - showGlobalErrorMessage(`JavaScript promise rejection: ${e.reason}. Open browser console to see more details.`); - return; - } - if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) { - // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240 - // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0. - return; // ignore such nonsense error event +function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}) { + const err = error ?? reason; + const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin)); + const {runModeIsProd} = window.config ?? {}; + + // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likly a + // non-critical event from the browser. We log them but don't show them to users. Examples: + // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors + // - https://github.com/mozilla-mobile/firefox-ios/issues/10817 + // - https://github.com/go-gitea/gitea/issues/20240 + if (!err) { + if (message) console.error(new Error(message)); + if (runModeIsProd) return; } - showGlobalErrorMessage(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`); + // If the error stack trace does not include the base URL of our script assets, it likely came + // from a browser extension or inline script. Do not show such errors in production. + if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) { + return; + } + + let msg = err?.message ?? message; + if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`; + const dot = msg.endsWith('.') ? '' : '.'; + const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type; + showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`); } function initGlobalErrorHandler() { @@ -50,13 +81,14 @@ function initGlobalErrorHandler() { if (!window.config) { showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); } - // we added an event handler for window error at the very beginning of <script> of page head - // the handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before this init - // then in this init, we can collect all error events and show them + // we added an event handler for window error at the very beginning of <script> of page head the + // handler calls `_globalHandlerErrors.push` (array method) to record all errors occur before + // this init then in this init, we can collect all error events and show them. for (const e of window._globalHandlerErrors || []) { processWindowErrorEvent(e); } - // then, change _globalHandlerErrors to an object with push method, to process further error events directly + // then, change _globalHandlerErrors to an object with push method, to process further error + // events directly window._globalHandlerErrors = {_inited: true, push: (e) => processWindowErrorEvent(e)}; } diff --git a/web_src/js/components/.eslintrc.yaml b/web_src/js/components/.eslintrc.yaml index 0cab470f6b..0d233442bc 100644 --- a/web_src/js/components/.eslintrc.yaml +++ b/web_src/js/components/.eslintrc.yaml @@ -7,6 +7,10 @@ extends: - plugin:vue/vue3-recommended - plugin:vue-scoped-css/vue3-recommended +parserOptions: + sourceType: module + ecmaVersion: latest + env: browser: true diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue index 51a7745431..7ada543fea 100644 --- a/web_src/js/components/ActionRunStatus.vue +++ b/web_src/js/components/ActionRunStatus.vue @@ -10,25 +10,25 @@ export default { props: { status: { type: String, - required: true + required: true, }, size: { type: Number, - default: 16 + default: 16, }, className: { type: String, - default: '' + default: '', }, localeStatus: { type: String, - default: '' - } + default: '', + }, }, }; </script> <template> - <span class="gt-df gt-ac" :data-tooltip-content="localeStatus" v-if="status"> + <span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status"> <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/> <SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/> <SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/> diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index 96a6e68012..9592a0df3c 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -11,7 +11,7 @@ export default { locale: { type: Object, default: () => {}, - } + }, }, data: () => ({ colorRange: [ @@ -49,7 +49,7 @@ export default { const newSearch = params.toString(); window.location.search = newSearch.length ? `?${newSearch}` : ''; - } + }, }, }; </script> diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index d9e6da316c..65a6089522 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -1,8 +1,7 @@ <script> -import $ from 'jquery'; import {SvgIcon} from '../svg.js'; -import {useLightTextOnBackground} from '../utils/color.js'; -import tinycolor from 'tinycolor2'; +import {contrastColor} from '../utils/color.js'; +import {GET} from '../modules/fetch.js'; const {appSubUrl, i18n} = window.config; @@ -59,17 +58,12 @@ export default { }, labels() { - return this.issue.labels.map((label) => { - let textColor; - const {r, g, b} = tinycolor(label.color).toRgb(); - if (useLightTextOnBackground(r, g, b)) { - textColor = '#eeeeee'; - } else { - textColor = '#111111'; - } - return {name: label.name, color: `#${label.color}`, textColor}; - }); - } + return this.issue.labels.map((label) => ({ + name: label.name, + color: `#${label.color}`, + textColor: contrastColor(`#${label.color}`), + })); + }, }, mounted() { this.$refs.root.addEventListener('ce-load-context-popup', (e) => { @@ -80,32 +74,35 @@ export default { }); }, methods: { - load(data) { + async load(data) { this.loading = true; this.i18nErrorMessage = null; - $.get(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`).done((issue) => { - this.issue = issue; - }).fail((jqXHR) => { - if (jqXHR.responseJSON && jqXHR.responseJSON.message) { - this.i18nErrorMessage = jqXHR.responseJSON.message; - } else { - this.i18nErrorMessage = i18n.network_error; + + try { + const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); + const respJson = await response.json(); + if (!response.ok) { + this.i18nErrorMessage = respJson.message ?? i18n.network_error; + return; } - }).always(() => { + this.issue = respJson; + } catch { + this.i18nErrorMessage = i18n.network_error; + } finally { this.loading = false; - }); - } - } + } + }, + }, }; </script> <template> <div ref="root"> - <div v-if="loading" class="ui active centered inline loader"/> + <div v-if="loading" class="tw-h-12 tw-w-12 is-loading"/> <div v-if="!loading && issue !== null"> <p><small>{{ issue.repository.full_name }} on {{ createdAt }}</small></p> <p><svg-icon :name="icon" :class="['text', color]"/> <strong>{{ issue.title }}</strong> #{{ issue.number }}</p> <p>{{ body }}</p> - <div> + <div class="labels-list"> <div v-for="label in labels" :key="label.name" diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 6f742bbea0..2d980a1b18 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -235,7 +235,7 @@ const sfc = { if (!this.reposTotalCount) { const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; response = await GET(totalCountSearchURL); - this.reposTotalCount = response.headers.get('X-Total-Count'); + this.reposTotalCount = response.headers.get('X-Total-Count') ?? '?'; } response = await GET(searchedURL); @@ -253,7 +253,7 @@ const sfc = { ...webSearchRepo.repository, latest_commit_status_state: webSearchRepo.latest_commit_status.State, locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status, - latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL + latest_commit_status_state_link: webSearchRepo.latest_commit_status.TargetURL, }; }); const count = response.headers.get('X-Total-Count'); @@ -325,7 +325,7 @@ const sfc = { if (this.activeIndex === -1 || this.activeIndex > this.repos.length - 1) { this.activeIndex = 0; } - } + }, }, }; @@ -345,19 +345,19 @@ export default sfc; // activate the IDE's Vue plugin <a :class="{item: true, active: tab === 'organizations'}" @click="changeTab('organizations')">{{ textOrganization }}</a> </div> <div v-show="tab === 'repos'" class="ui tab active list dashboard-repos"> - <h4 class="ui top attached header gt-df gt-ac"> - <div class="gt-f1 gt-df gt-ac"> + <h4 class="ui top attached header tw-flex tw-items-center"> + <div class="tw-flex-1 tw-flex tw-items-center"> {{ textMyRepos }} - <span class="ui grey label gt-ml-3">{{ reposTotalCount }}</span> + <span class="ui grey label tw-ml-2">{{ reposTotalCount }}</span> </div> - <a class="gt-df gt-ac muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo"> + <a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo"> <svg-icon name="octicon-plus"/> </a> </h4> <div class="ui attached segment repos-search"> - <div class="ui fluid action left icon input" :class="{loading: isLoading}"> + <div class="ui small fluid action left icon input"> <input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos"> - <i class="icon"><svg-icon name="octicon-search" :size="16"/></i> + <i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i> <div class="ui dropdown icon button" :title="textFilter"> <svg-icon name="octicon-filter" :size="16"/> <div class="menu"> @@ -367,7 +367,7 @@ export default sfc; // activate the IDE's Vue plugin otherwise if the "input" handles click event for intermediate status, it breaks the internal state--> <input type="checkbox" class="hidden" v-bind.prop="checkboxArchivedFilterProps"> <label> - <svg-icon name="octicon-archive" :size="16" class-name="gt-mr-2"/> + <svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/> {{ textShowArchived }} </label> </div> @@ -376,7 +376,7 @@ export default sfc; // activate the IDE's Vue plugin <div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle"> <input type="checkbox" class="hidden" v-bind.prop="checkboxPrivateFilterProps"> <label> - <svg-icon name="octicon-lock" :size="16" class-name="gt-mr-2"/> + <svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/> {{ textShowPrivate }} </label> </div> @@ -384,32 +384,34 @@ export default sfc; // activate the IDE's Vue plugin </div> </div> </div> - <div class="ui secondary tiny pointing borderless menu center grid repos-filter"> - <a class="item" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')"> - {{ textAll }} - <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div> - </a> - <a class="item" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')"> - {{ textSources }} - <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div> - </a> - <a class="item" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')"> - {{ textForks }} - <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div> - </a> - <a class="item" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled"> - {{ textMirrors }} - <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div> - </a> - <a class="item" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')"> - {{ textCollaborative }} - <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div> - </a> - </div> + <overflow-menu class="ui secondary pointing tabular borderless menu repos-filter"> + <div class="overflow-menu-items tw-justify-center"> + <a class="item" tabindex="0" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')"> + {{ textAll }} + <div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div> + </a> + <a class="item" tabindex="0" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')"> + {{ textSources }} + <div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div> + </a> + <a class="item" tabindex="0" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')"> + {{ textForks }} + <div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div> + </a> + <a class="item" tabindex="0" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled"> + {{ textMirrors }} + <div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div> + </a> + <a class="item" tabindex="0" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')"> + {{ textCollaborative }} + <div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div> + </a> + </div> + </overflow-menu> </div> - <div v-if="repos.length" class="ui attached table segment gt-rounded-bottom"> + <div v-if="repos.length" class="ui attached table segment tw-rounded-b"> <ul class="repo-owner-name-list"> - <li class="gt-df gt-ac gt-py-3" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id"> + <li class="tw-flex tw-items-center tw-py-2" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id"> <a class="repo-list-link muted" :href="repo.link"> <svg-icon :name="repoIcon(repo)" :size="16" class-name="repo-list-icon"/> <div class="text truncate">{{ repo.full_name }}</div> @@ -417,56 +419,57 @@ export default sfc; // activate the IDE's Vue plugin <svg-icon name="octicon-archive" :size="16"/> </div> </a> - <a class="gt-df gt-ac" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state"> + <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state"> <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl --> - <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'gt-ml-3 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/> + <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/> </a> </li> </ul> - <div v-if="showMoreReposLink" class="center gt-py-3 gt-border-secondary-top"> - <div class="ui borderless pagination menu narrow"> + <div v-if="showMoreReposLink" class="tw-text-center"> + <div class="divider tw-my-0"/> + <div class="ui borderless pagination menu narrow tw-my-2"> <a - class="item navigation gt-py-2" :class="{'disabled': page === 1}" + class="item navigation tw-py-1" :class="{'disabled': page === 1}" @click="changePage(1)" :title="textFirstPage" > - <svg-icon name="gitea-double-chevron-left" :size="16" class-name="gt-mr-2"/> + <svg-icon name="gitea-double-chevron-left" :size="16" class-name="tw-mr-1"/> </a> <a - class="item navigation gt-py-2" :class="{'disabled': page === 1}" + class="item navigation tw-py-1" :class="{'disabled': page === 1}" @click="changePage(page - 1)" :title="textPreviousPage" > - <svg-icon name="octicon-chevron-left" :size="16" clsas-name="gt-mr-2"/> + <svg-icon name="octicon-chevron-left" :size="16" clsas-name="tw-mr-1"/> </a> - <a class="active item gt-py-2">{{ page }}</a> + <a class="active item tw-py-1">{{ page }}</a> <a class="item navigation" :class="{'disabled': page === finalPage}" @click="changePage(page + 1)" :title="textNextPage" > - <svg-icon name="octicon-chevron-right" :size="16" class-name="gt-ml-2"/> + <svg-icon name="octicon-chevron-right" :size="16" class-name="tw-ml-1"/> </a> <a - class="item navigation gt-py-2" :class="{'disabled': page === finalPage}" + class="item navigation tw-py-1" :class="{'disabled': page === finalPage}" @click="changePage(finalPage)" :title="textLastPage" > - <svg-icon name="gitea-double-chevron-right" :size="16" class-name="gt-ml-2"/> + <svg-icon name="gitea-double-chevron-right" :size="16" class-name="tw-ml-1"/> </a> </div> </div> </div> </div> <div v-if="!isOrganization" v-show="tab === 'organizations'" class="ui tab active list dashboard-orgs"> - <h4 class="ui top attached header gt-df gt-ac"> - <div class="gt-f1 gt-df gt-ac"> + <h4 class="ui top attached header tw-flex tw-items-center"> + <div class="tw-flex-1 tw-flex tw-items-center"> {{ textMyOrgs }} - <span class="ui grey label gt-ml-3">{{ organizationsTotalCount }}</span> + <span class="ui grey label tw-ml-2">{{ organizationsTotalCount }}</span> </div> - <a class="gt-df gt-ac muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg"> + <a class="tw-flex tw-items-center muted" v-if="canCreateOrganization" :href="subUrl + '/org/create'" :data-tooltip-content="textNewOrg"> <svg-icon name="octicon-plus"/> </a> </h4> - <div v-if="organizations.length" class="ui attached table segment gt-rounded-bottom"> + <div v-if="organizations.length" class="ui attached table segment tw-rounded-b"> <ul class="repo-owner-name-list"> - <li class="gt-df gt-ac gt-py-3" v-for="org in organizations" :key="org.name"> + <li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name"> <a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)"> <svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/> <div class="text truncate">{{ org.name }}</div> @@ -476,9 +479,9 @@ export default sfc; // activate the IDE's Vue plugin </span> </div> </a> - <div class="text light grey gt-df gt-ac gt-ml-3"> + <div class="text light grey tw-flex tw-items-center tw-ml-2"> {{ org.num_repos }} - <svg-icon name="octicon-repo" :size="16" class-name="gt-ml-2 gt-mt-1"/> + <svg-icon name="octicon-repo" :size="16" class-name="tw-ml-1 tw-mt-0.5"/> </div> </li> </ul> @@ -501,6 +504,22 @@ ul li:not(:last-child) { border-bottom: 1px solid var(--color-secondary); } +.repos-search { + padding-bottom: 0 !important; +} + +.repos-filter { + padding-top: 0 !important; + margin-top: 0 !important; + border-bottom-width: 0 !important; + margin-bottom: 2px !important; +} + +.repos-filter .item { + padding-left: 6px !important; + padding-right: 6px !important; +} + .repo-list-link { min-width: 0; /* for text truncation */ display: flex; diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index 54877a18c0..352d085731 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -14,7 +14,7 @@ export default { }, commits: [], hoverActivated: false, - lastReviewCommitSha: null + lastReviewCommitSha: null, }; }, computed: { @@ -29,7 +29,7 @@ export default { }, issueLink() { return this.$el.parentNode.getAttribute('data-issuelink'); - } + }, }, mounted() { document.body.addEventListener('click', this.onBodyClick); @@ -103,7 +103,7 @@ export default { this.menuVisible = !this.menuVisible; // load our commits when the menu is not yet visible (it'll be toggled after loading) // and we got no commits - if (this.commits.length === 0 && this.menuVisible && !this.isLoading) { + if (!this.commits.length && this.menuVisible && !this.isLoading) { this.isLoading = true; try { await this.fetchCommits(); @@ -185,7 +185,7 @@ export default { } } }, - } + }, }; </script> <template> @@ -204,19 +204,19 @@ export default { </button> <div class="menu left transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'"> <div class="loading-indicator is-loading" v-if="isLoading"/> - <div v-if="!isLoading" class="vertical item gt-df gt-fc gt-gap-2" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()"> + <div v-if="!isLoading" class="vertical item" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()"> <div class="gt-ellipsis"> {{ locale.show_all_commits }} </div> - <div class="gt-ellipsis text light-2 gt-mb-0"> + <div class="gt-ellipsis text light-2 tw-mb-0"> {{ locale.stats_num_commits }} </div> </div> <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review --> <div v-if="lastReviewCommitSha != null" role="menuitem" - class="vertical item gt-df gt-fc gt-gap-2 gt-border-secondary-top" - :class="{disabled: commitsSinceLastReview === 0}" + class="vertical item" + :class="{disabled: !commitsSinceLastReview}" @keydown.enter="changesSinceLastReviewClick()" @click="changesSinceLastReviewClick()" > @@ -227,10 +227,10 @@ export default { {{ commitsSinceLastReview }} commits </div> </div> - <span v-if="!isLoading" class="info gt-border-secondary-top text light-2">{{ locale.select_commit_hold_shift_for_range }}</span> + <span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span> <template v-for="commit in commits" :key="commit.id"> <div - class="vertical item gt-df gt-gap-2 gt-border-secondary-top" role="menuitem" + class="vertical item" role="menuitem" :class="{selection: commit.selected, hovered: commit.hovered}" @keydown.enter.exact="commitClicked(commit.id)" @keydown.enter.shift.exact="commitClickedShift(commit)" @@ -240,7 +240,7 @@ export default { @click.meta.exact="commitClicked(commit.id, true)" @click.shift.exact.stop.prevent="commitClickedShift(commit)" > - <div class="gt-f1 gt-df gt-fc gt-gap-2"> + <div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1"> <div class="gt-ellipsis commit-list-summary"> {{ commit.summary }} </div> @@ -248,11 +248,11 @@ export default { {{ commit.committer_or_author_name }} <span class="text right"> <!-- TODO: make this respect the PreferredTimestampTense setting --> - <relative-time class="time-since" prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time> + <relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time> </span> </div> </div> - <div class="gt-mono"> + <div class="tw-font-mono"> {{ commit.short_sha }} </div> </div> @@ -285,10 +285,14 @@ export default { width: 350px; } - #diff-commit-selector-menu .item { + #diff-commit-selector-menu .item, + #diff-commit-selector-menu .info { + display: flex !important; flex-direction: row; line-height: 1.4; padding: 7px 14px !important; + border-top: 1px solid var(--color-secondary) !important; + gap: 0.25em; } #diff-commit-selector-menu .item:focus { diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue index 8bde61804f..916780d913 100644 --- a/web_src/js/components/DiffFileList.vue +++ b/web_src/js/components/DiffFileList.vue @@ -31,26 +31,26 @@ export default { }, loadMoreData() { loadMoreFiles(this.store.linkLoadMore); - } + }, }, }; </script> <template> - <ol class="diff-stats gt-m-0" ref="root" v-if="store.fileListIsVisible"> + <ol class="diff-stats tw-m-0" ref="root" v-if="store.fileListIsVisible"> <li v-for="file in store.files" :key="file.NameHash"> - <div class="gt-font-semibold gt-df gt-ac pull-right"> - <span v-if="file.IsBin" class="gt-ml-1 gt-mr-3">{{ store.binaryFileMessage }}</span> + <div class="tw-font-semibold tw-flex tw-items-center pull-right"> + <span v-if="file.IsBin" class="tw-ml-0.5 tw-mr-2">{{ store.binaryFileMessage }}</span> {{ file.IsBin ? '' : file.Addition + file.Deletion }} - <span v-if="!file.IsBin" class="diff-stats-bar gt-mx-3" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)"> + <span v-if="!file.IsBin" class="diff-stats-bar tw-mx-2" :data-tooltip-content="store.statisticsMessage.replace('%d', (file.Addition + file.Deletion)).replace('%d', file.Addition).replace('%d', file.Deletion)"> <div class="diff-stats-add-bar" :style="{ 'width': diffStatsWidth(file.Addition, file.Deletion) }"/> </span> </div> <!-- todo finish all file status, now modify, add, delete and rename --> <span :class="['status', diffTypeToString(file.Type)]" :data-tooltip-content="diffTypeToString(file.Type)"> </span> - <a class="file gt-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a> + <a class="file tw-font-mono" :href="'#diff-' + file.NameHash">{{ file.Name }}</a> </li> - <li v-if="store.isIncomplete" class="gt-pt-2"> - <span class="file gt-df gt-ac gt-sb">{{ store.tooManyFilesMessage }} + <li v-if="store.isIncomplete" class="tw-pt-1"> + <span class="file tw-flex tw-items-center tw-justify-between">{{ store.tooManyFilesMessage }} <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a> </span> </li> diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 3686629df8..cddfee1e04 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -30,7 +30,7 @@ export default { let newParent = { name: split, children: [], - isFile + isFile, }; if (isFile === true) { @@ -40,7 +40,7 @@ export default { if (parent) { // check if the folder already exists const existingFolder = parent.children.find( - (x) => x.name === split + (x) => x.name === split, ); if (existingFolder) { newParent = existingFolder; @@ -74,7 +74,7 @@ export default { // reduce the depth of our tree. mergeChildIfOnlyOneDir(result); return result; - } + }, }, mounted() { // Default to true if unset @@ -129,7 +129,7 @@ export default { <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items"> <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> <DiffFileTreeItem v-for="item in fileTree" :key="item.name" :item="item"/> - <div v-if="store.isIncomplete" class="gt-pt-2"> + <div v-if="store.isIncomplete" class="tw-pt-1"> <a :class="['ui', 'basic', 'tiny', 'button', store.isLoadingNewData ? 'disabled' : '']" @click.stop="loadMoreData">{{ store.showMoreMessage }}</a> </div> </div> diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index 9d7ab4afc5..0f6e54363f 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -7,7 +7,7 @@ export default { props: { item: { type: Object, - required: true + required: true, }, }, data: () => ({ @@ -37,7 +37,7 @@ export default { > <!-- file --> <SvgIcon name="octicon-file"/> - <span class="gt-ellipsis gt-f1">{{ item.name }}</span> + <span class="gt-ellipsis tw-flex-1">{{ item.name }}</span> <SvgIcon :name="getIconForDiffType(item.file.Type).name" :class="getIconForDiffType(item.file.Type).classes"/> </a> <div v-else class="item-directory" :title="item.name" @click.stop="collapsed = !collapsed"> diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index b0b10b6252..bd0901a7b5 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -43,7 +43,7 @@ export default { for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); } - } + }, }, created() { this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); @@ -94,6 +94,7 @@ export default { <!-- eslint-disable-next-line vue/no-v-html --> <div v-if="mergeForm.hasPendingPullRequestMerge" v-html="mergeForm.hasPendingPullRequestMergeTip" class="ui info message"/> + <!-- another similar form is in pull.tmpl (manual merge)--> <form class="ui form form-fetch-action" v-if="showActionForm" :action="mergeForm.baseLink+'/merge'" method="post"> <input type="hidden" name="_csrf" :value="csrfToken"> <input type="hidden" name="head_commit_id" v-model="mergeForm.pullHeadCommitID"> @@ -107,7 +108,7 @@ export default { <div class="field"> <textarea name="merge_message_field" rows="5" :placeholder="mergeForm.mergeMessageFieldPlaceHolder" v-model="mergeMessageFieldValue"/> <template v-if="mergeMessageFieldValue !== mergeForm.defaultMergeMessage"> - <button @click.prevent="clearMergeMessage" class="btn gt-mt-2 gt-p-2 interact-fg" :data-tooltip-content="mergeForm.textClearMergeMessageHint"> + <button @click.prevent="clearMergeMessage" class="btn tw-mt-1 tw-p-1 interact-fg" :data-tooltip-content="mergeForm.textClearMergeMessageHint"> {{ mergeForm.textClearMergeMessage }} </button> </template> @@ -129,13 +130,13 @@ export default { {{ mergeForm.textCancel }} </button> - <div class="ui checkbox gt-ml-2" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed"> + <div class="ui checkbox tw-ml-1" v-if="mergeForm.isPullBranchDeletable && !autoMergeWhenSucceed"> <input name="delete_branch_after_merge" type="checkbox" v-model="deleteBranchAfterMerge" id="delete-branch-after-merge"> <label for="delete-branch-after-merge">{{ mergeForm.textDeleteBranch }}</label> </div> </form> - <div v-if="!showActionForm" class="gt-df"> + <div v-if="!showActionForm" class="tw-flex"> <!-- the merge button --> <div class="ui buttons merge-button" :class="[mergeForm.emptyCommit ? 'grey' : mergeForm.allOverridableChecksOk ? 'primary' : 'red']" @click="toggleActionForm(true)"> <button class="ui button"> @@ -176,7 +177,7 @@ export default { </div> <!-- the cancel auto merge button --> - <form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="gt-ml-4"> + <form v-if="mergeForm.hasPendingPullRequestMerge" :action="mergeForm.baseLink+'/cancel_auto_merge'" method="post" class="tw-ml-4"> <input type="hidden" name="_csrf" :value="csrfToken"> <button class="ui button"> {{ mergeForm.textAutoMergeCancelSchedule }} diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 797869b78c..06c42f0b35 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -3,9 +3,9 @@ import {SvgIcon} from '../svg.js'; import ActionRunStatus from './ActionRunStatus.vue'; import {createApp} from 'vue'; import {toggleElem} from '../utils/dom.js'; -import {getCurrentLocale} from '../utils.js'; +import {formatDatetime} from '../utils/time.js'; import {renderAnsi} from '../render/ansi.js'; -import {POST} from '../modules/fetch.js'; +import {GET, POST, DELETE} from '../modules/fetch.js'; const sfc = { name: 'RepoActionView', @@ -66,7 +66,7 @@ const sfc = { name: '', link: '', }, - } + }, }, currentJob: { title: '', @@ -167,7 +167,7 @@ const sfc = { const logTimeStamp = document.createElement('span'); logTimeStamp.className = 'log-time-stamp'; const date = new Date(parseFloat(line.timestamp * 1000)); - const timeStamp = date.toLocaleString(getCurrentLocale(), {timeZoneName: 'short'}); + const timeStamp = formatDatetime(date); logTimeStamp.textContent = timeStamp; toggleElem(logTimeStamp, this.timeVisible['log-time-stamp']); // for "Show seconds" @@ -196,10 +196,16 @@ const sfc = { }, async fetchArtifacts() { - const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/artifacts`); + const resp = await GET(`${this.actionsURL}/runs/${this.runIndex}/artifacts`); return await resp.json(); }, + async deleteArtifact(name) { + if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return; + await DELETE(`${this.run.link}/artifacts/${name}`); + await this.loadJob(); + }, + async fetchJob() { const logCursors = this.currentJobStepsStates.map((it, idx) => { // cursor is used to indicate the last position of the logs @@ -262,6 +268,10 @@ const sfc = { return ['success', 'skipped', 'failure', 'cancelled'].includes(status); }, + isExpandable(status) { + return ['success', 'running', 'failure', 'cancelled'].includes(status); + }, + closeDropdown() { if (this.menuVisible) this.menuVisible = false; }, @@ -305,7 +315,7 @@ const sfc = { const logLine = this.$refs.steps.querySelector(selectedLogStep); if (!logLine) return; logLine.querySelector('.line-num').click(); - } + }, }, }; @@ -329,6 +339,8 @@ export function initRepositoryActionView() { cancel: el.getAttribute('data-locale-cancel'), rerun: el.getAttribute('data-locale-rerun'), artifactsTitle: el.getAttribute('data-locale-artifacts-title'), + areYouSure: el.getAttribute('data-locale-are-you-sure'), + confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), rerun_all: el.getAttribute('data-locale-rerun-all'), showTimeStamps: el.getAttribute('data-locale-show-timestamps'), showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), @@ -344,7 +356,7 @@ export function initRepositoryActionView() { skipped: el.getAttribute('data-locale-status-skipped'), blocked: el.getAttribute('data-locale-status-blocked'), }, - } + }, }); view.mount(el); } @@ -365,7 +377,7 @@ export function initRepositoryActionView() { <button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel"> {{ locale.cancel }} </button> - <button class="ui basic small compact button gt-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun"> + <button class="ui basic small compact button tw-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun"> {{ locale.rerun_all }} </button> </div> @@ -386,10 +398,10 @@ export function initRepositoryActionView() { <a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="parseInt(jobIndex) === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id" @mouseenter="onHoverRerunIndex = job.id" @mouseleave="onHoverRerunIndex = -1"> <div class="job-brief-item-left"> <ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/> - <span class="job-brief-name gt-mx-3 gt-ellipsis">{{ job.name }}</span> + <span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span> </div> <span class="job-brief-item-right"> - <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun gt-mx-3 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/> + <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/> <span class="step-summary-duration">{{ job.duration }}</span> </span> </a> @@ -404,6 +416,9 @@ export function initRepositoryActionView() { <a class="job-artifacts-link" target="_blank" :href="run.link+'/artifacts/'+artifact.name"> <SvgIcon name="octicon-file" class="ui text black job-artifacts-icon"/>{{ artifact.name }} </a> + <a v-if="run.canDeleteArtifact" @click="deleteArtifact(artifact.name)" class="job-artifacts-delete"> + <SvgIcon name="octicon-trash" class="ui text black job-artifacts-icon"/> + </a> </li> </ul> </div> @@ -421,7 +436,7 @@ export function initRepositoryActionView() { </div> <div class="job-info-header-right"> <div class="ui top right pointing dropdown custom jump item" @click.stop="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> - <button class="btn gt-interact-bg gt-p-3"> + <button class="btn gt-interact-bg tw-p-2"> <SvgIcon name="octicon-gear" :size="18"/> </button> <div class="menu transition action-job-menu" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak> @@ -438,7 +453,7 @@ export function initRepositoryActionView() { {{ locale.showFullScreen }} </a> <div class="divider"/> - <a :class="['item', currentJob.steps.length === 0 ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank"> + <a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank"> <i class="icon"><SvgIcon name="octicon-download"/></i> {{ locale.downloadLogs }} </a> @@ -446,15 +461,15 @@ export function initRepositoryActionView() { </div> </div> </div> - <div class="job-step-container" ref="steps"> + <div class="job-step-container" ref="steps" v-if="currentJob.steps.length"> <div class="job-step-section" v-for="(jobStep, i) in currentJob.steps" :key="i"> - <div class="job-step-summary" @click.stop="toggleStepLogs(i)" :class="currentJobStepsStates[i].expanded ? 'selected' : ''"> + <div class="job-step-summary" @click.stop="isExpandable(jobStep.status) && toggleStepLogs(i)" :class="[currentJobStepsStates[i].expanded ? 'selected' : '', isExpandable(jobStep.status) && 'step-expandable']"> <!-- If the job is done and the job step log is loaded for the first time, show the loading icon currentJobStepsStates[i].cursor === null means the log is loaded for the first time --> - <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="gt-mr-3 job-status-rotate"/> - <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" class="gt-mr-3"/> - <ActionRunStatus :status="jobStep.status" class="gt-mr-3"/> + <SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="tw-mr-2 job-status-rotate"/> + <SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['tw-mr-2', !isExpandable(jobStep.status) && 'tw-invisible']"/> + <ActionRunStatus :status="jobStep.status" class="tw-mr-2"/> <span class="step-summary-msg gt-ellipsis">{{ jobStep.summary }}</span> <span class="step-summary-duration">{{ jobStep.duration }}</span> @@ -502,8 +517,16 @@ export function initRepositoryActionView() { .action-commit-summary { display: flex; + flex-wrap: wrap; gap: 5px; - margin: 0 0 0 28px; + margin-left: 28px; +} + +@media (max-width: 767.98px) { + .action-commit-summary { + margin-left: 0; + margin-top: 8px; + } } /* ================ */ @@ -513,9 +536,17 @@ export function initRepositoryActionView() { width: 30%; max-width: 400px; position: sticky; - top: 0; + top: 12px; max-height: 100vh; overflow-y: auto; + background: var(--color-body); + z-index: 2; /* above .job-info-header */ +} + +@media (max-width: 767.98px) { + .action-view-left { + position: static; /* can not sticky because multiple jobs would overlap into right view */ + } } .job-artifacts-title { @@ -528,6 +559,8 @@ export function initRepositoryActionView() { .job-artifacts-item { margin: 5px 0; padding: 6px; + display: flex; + justify-content: space-between; } .job-artifacts-list { @@ -609,6 +642,10 @@ export function initRepositoryActionView() { width: 70%; display: flex; flex-direction: column; + border: 1px solid var(--color-console-border); + border-radius: var(--border-radius); + background: var(--color-console-bg); + align-self: flex-start; } /* begin fomantic button overrides */ @@ -668,13 +705,16 @@ export function initRepositoryActionView() { justify-content: space-between; align-items: center; padding: 0 12px; - border-bottom: 1px solid var(--color-console-border); - background-color: var(--color-console-bg); position: sticky; top: 0; - border-radius: var(--border-radius) var(--border-radius) 0 0; height: 60px; - z-index: 1; + z-index: 1; /* above .job-step-container */ + background: var(--color-console-bg); + border-radius: 3px; +} + +.job-info-header:has(+ .job-step-container) { + border-radius: var(--border-radius) var(--border-radius) 0 0; } .job-info-header .job-info-header-title { @@ -689,20 +729,28 @@ export function initRepositoryActionView() { } .job-step-container { - background-color: var(--color-console-bg); max-height: 100%; border-radius: 0 0 var(--border-radius) var(--border-radius); + border-top: 1px solid var(--color-console-border); z-index: 0; } .job-step-container .job-step-summary { - cursor: pointer; padding: 5px 10px; display: flex; align-items: center; border-radius: var(--border-radius); } +.job-step-container .job-step-summary.step-expandable { + cursor: pointer; +} + +.job-step-container .job-step-summary.step-expandable:hover { + color: var(--color-console-fg); + background: var(--color-console-hover-bg); +} + .job-step-container .job-step-summary .step-summary-msg { flex: 1; } @@ -711,12 +759,6 @@ export function initRepositoryActionView() { margin-left: 16px; } -.job-step-container .job-step-summary:hover { - color: var(--color-console-fg); - background-color: var(--color-console-hover-bg); - -} - .job-step-container .job-step-summary.selected { color: var(--color-console-fg); background-color: var(--color-console-active-bg); @@ -724,17 +766,15 @@ export function initRepositoryActionView() { top: 60px; } -@media (max-width: 768px) { +@media (max-width: 767.98px) { .action-view-body { flex-direction: column; } .action-view-left, .action-view-right { width: 100%; } - .action-view-left { max-width: none; - overflow-y: hidden; } } </style> @@ -747,7 +787,7 @@ export function initRepositoryActionView() { @keyframes job-status-rotate-keyframes { 100% { - transform: rotate(360deg); + transform: rotate(-360deg); } } @@ -777,7 +817,7 @@ export function initRepositoryActionView() { /* class names 'log-time-seconds' and 'log-time-stamp' are used in the method toggleTimeDisplay */ .job-log-line .line-num, .log-time-seconds { width: 48px; - color: var(--color-grey-light); + color: var(--color-text-light-3); text-align: right; user-select: none; } @@ -793,7 +833,7 @@ export function initRepositoryActionView() { .job-log-line .log-time, .log-time-stamp { - color: var(--color-grey-light); + color: var(--color-text-light-3); margin-left: 10px; white-space: nowrap; } diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index fe41218d88..a41fb61d78 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -47,7 +47,7 @@ const sfc = { this.colors.barColor = refStyle.backgroundColor; this.colors.textColor = refStyle.color; this.colors.textAltColor = refAltStyle.color; - } + }, }; export function initRepoActivityTopAuthorsChart() { diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index bc7d979d99..4e977ab185 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -19,24 +19,26 @@ const sfc = { }); // TODO: fix this anti-pattern: side-effects-in-computed-properties - this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); + this.active = !items.length && this.showCreateNewBranch ? 0 : -1; return items; }, showNoResults() { - return this.filteredItems.length === 0 && !this.showCreateNewBranch; + return !this.filteredItems.length && !this.showCreateNewBranch; }, showCreateNewBranch() { if (this.disableCreateBranch || !this.searchTerm) { return false; } - return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; + return !this.items.filter((item) => { + return item.name.toLowerCase() === this.searchTerm.toLowerCase(); + }).length; }, formActionUrl() { return `${this.repoLink}/branches/_new/${this.branchNameSubURL}`; }, shouldCreateTag() { return this.mode === 'tags'; - } + }, }, watch: { @@ -45,7 +47,7 @@ const sfc = { this.focusSearchField(); this.fetchBranchesOrTags(); } - } + }, }, beforeMount() { @@ -83,7 +85,7 @@ const sfc = { this.isViewBranch = false; this.$refs.dropdownRefName.textContent = item.name; if (this.setAction) { - $(`#${this.branchForm}`).attr('action', url); + document.getElementById(this.branchForm)?.setAttribute('action', url); } else { $(`#${this.branchForm} input[name="refURL"]`).val(url); } @@ -123,7 +125,7 @@ const sfc = { return -1; }, scrollToActive() { - let el = this.$refs[`listItem${this.active}`]; + let el = this.$refs[`listItem${this.active}`]; // eslint-disable-line no-jquery/variable-pattern if (!el || !el.length) return; if (Array.isArray(el)) { el = el[0]; @@ -209,7 +211,7 @@ const sfc = { this.isLoading = false; } }, - } + }, }; export function initRepoBranchTagSelector(selector) { @@ -245,13 +247,13 @@ export default sfc; // activate IDE's Vue plugin </script> <template> <div class="ui dropdown custom"> - <button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> - <span class="text gt-df gt-ac gt-mr-2"> + <button class="branch-dropdown-button gt-ellipsis ui basic small compact button tw-flex tw-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> + <span class="text tw-flex tw-items-center tw-mr-1"> <template v-if="release">{{ textReleaseCompare }}</template> <template v-else> <svg-icon v-if="isViewTag" name="octicon-tag"/> <svg-icon v-else name="octicon-git-branch"/> - <strong ref="dropdownRefName" class="gt-ml-3">{{ refNameText }}</strong> + <strong ref="dropdownRefName" class="tw-ml-2">{{ refNameText }}</strong> </template> </span> <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/> @@ -263,10 +265,10 @@ export default sfc; // activate IDE's Vue plugin </div> <div v-if="showBranchesInDropdown" class="branch-tag-tab"> <a class="branch-tag-item muted" :class="{active: mode === 'branches'}" href="#" @click="handleTabSwitch('branches')"> - <svg-icon name="octicon-git-branch" :size="16" class-name="gt-mr-2"/>{{ textBranches }} + <svg-icon name="octicon-git-branch" :size="16" class-name="tw-mr-1"/>{{ textBranches }} </a> <a v-if="!noTag" class="branch-tag-item muted" :class="{active: mode === 'tags'}" href="#" @click="handleTabSwitch('tags')"> - <svg-icon name="octicon-tag" :size="16" class-name="gt-mr-2"/>{{ textTags }} + <svg-icon name="octicon-tag" :size="16" class-name="tw-mr-1"/>{{ textTags }} </a> </div> <div class="branch-tag-divider"/> @@ -278,7 +280,7 @@ export default sfc; // activate IDE's Vue plugin <div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'"> {{ textDefaultBranchLabel }} </div> - <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon gt-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop> + <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon tw-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop> <!-- creating a lot of Vue component is pretty slow, so we use a static SVG here --> <svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg> </a> diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue new file mode 100644 index 0000000000..adce431264 --- /dev/null +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -0,0 +1,172 @@ +<script> +import {SvgIcon} from '../svg.js'; +import { + Chart, + Legend, + LinearScale, + TimeScale, + PointElement, + LineElement, + Filler, +} from 'chart.js'; +import {GET} from '../modules/fetch.js'; +import {Line as ChartLine} from 'vue-chartjs'; +import { + startDaysBetween, + firstStartDateAfterDate, + fillEmptyStartDaysWithZeroes, +} from '../utils/time.js'; +import {chartJsColors} from '../utils/color.js'; +import {sleep} from '../utils.js'; +import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; + +const {pageData} = window.config; + +Chart.defaults.color = chartJsColors.text; +Chart.defaults.borderColor = chartJsColors.border; + +Chart.register( + TimeScale, + LinearScale, + Legend, + PointElement, + LineElement, + Filler, +); + +export default { + components: {ChartLine, SvgIcon}, + props: { + locale: { + type: Object, + required: true, + }, + }, + data: () => ({ + isLoading: false, + errorText: '', + repoLink: pageData.repoLink || [], + data: [], + }), + mounted() { + this.fetchGraphData(); + }, + methods: { + async fetchGraphData() { + this.isLoading = true; + try { + let response; + do { + response = await GET(`${this.repoLink}/activity/code-frequency/data`); + if (response.status === 202) { + await sleep(1000); // wait for 1 second before retrying + } + } while (response.status === 202); + if (response.ok) { + this.data = await response.json(); + const weekValues = Object.values(this.data); + const start = weekValues[0].week; + const end = firstStartDateAfterDate(new Date()); + const startDays = startDaysBetween(new Date(start), new Date(end)); + this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); + this.errorText = ''; + } else { + this.errorText = response.statusText; + } + } catch (err) { + this.errorText = err.message; + } finally { + this.isLoading = false; + } + }, + + toGraphData(data) { + return { + datasets: [ + { + data: data.map((i) => ({x: i.week, y: i.additions})), + pointRadius: 0, + pointHitRadius: 0, + fill: true, + label: 'Additions', + backgroundColor: chartJsColors['additions'], + borderWidth: 0, + tension: 0.3, + }, + { + data: data.map((i) => ({x: i.week, y: -i.deletions})), + pointRadius: 0, + pointHitRadius: 0, + fill: true, + label: 'Deletions', + backgroundColor: chartJsColors['deletions'], + borderWidth: 0, + tension: 0.3, + }, + ], + }; + }, + + getOptions() { + return { + responsive: true, + maintainAspectRatio: false, + animation: true, + plugins: { + legend: { + display: true, + }, + }, + scales: { + x: { + type: 'time', + grid: { + display: false, + }, + time: { + minUnit: 'month', + }, + ticks: { + maxRotation: 0, + maxTicksLimit: 12, + }, + }, + y: { + ticks: { + maxTicksLimit: 6, + }, + }, + }, + }; + }, + }, +}; +</script> +<template> + <div> + <div class="ui header tw-flex tw-items-center tw-justify-between"> + {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }} + </div> + <div class="tw-flex ui segment main-graph"> + <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto"> + <div v-if="isLoading"> + <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/> + {{ locale.loadingInfo }} + </div> + <div v-else class="text red"> + <SvgIcon name="octicon-x-circle-fill"/> + {{ errorText }} + </div> + </div> + <ChartLine + v-memo="data" v-if="data.length !== 0" + :data="toGraphData(data)" :options="getOptions()" + /> + </div> + </div> +</template> +<style scoped> +.main-graph { + height: 440px; +} +</style> diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue new file mode 100644 index 0000000000..2347c41ae4 --- /dev/null +++ b/web_src/js/components/RepoContributors.vue @@ -0,0 +1,432 @@ +<script> +import {SvgIcon} from '../svg.js'; +import { + Chart, + Title, + BarElement, + LinearScale, + TimeScale, + PointElement, + LineElement, + Filler, +} from 'chart.js'; +import {GET} from '../modules/fetch.js'; +import zoomPlugin from 'chartjs-plugin-zoom'; +import {Line as ChartLine} from 'vue-chartjs'; +import { + startDaysBetween, + firstStartDateAfterDate, + fillEmptyStartDaysWithZeroes, +} from '../utils/time.js'; +import {chartJsColors} from '../utils/color.js'; +import {sleep} from '../utils.js'; +import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; +import $ from 'jquery'; + +const {pageData} = window.config; + +const customEventListener = { + id: 'customEventListener', + afterEvent: (chart, args, opts) => { + // event will be replayed from chart.update when reset zoom, + // so we need to check whether args.replay is true to avoid call loops + if (args.event.type === 'dblclick' && opts.chartType === 'main' && !args.replay) { + chart.resetZoom(); + opts.instance.updateOtherCharts(args.event, true); + } + }, +}; + +Chart.defaults.color = chartJsColors.text; +Chart.defaults.borderColor = chartJsColors.border; + +Chart.register( + TimeScale, + LinearScale, + BarElement, + Title, + PointElement, + LineElement, + Filler, + zoomPlugin, + customEventListener, +); + +export default { + components: {ChartLine, SvgIcon}, + props: { + locale: { + type: Object, + required: true, + }, + }, + data: () => ({ + isLoading: false, + errorText: '', + totalStats: {}, + sortedContributors: {}, + repoLink: pageData.repoLink || [], + type: pageData.contributionType, + contributorsStats: [], + xAxisStart: null, + xAxisEnd: null, + xAxisMin: null, + xAxisMax: null, + }), + mounted() { + this.fetchGraphData(); + + $('#repo-contributors').dropdown({ + onChange: (val) => { + this.xAxisMin = this.xAxisStart; + this.xAxisMax = this.xAxisEnd; + this.type = val; + this.sortContributors(); + }, + }); + }, + methods: { + sortContributors() { + const contributors = this.filterContributorWeeksByDateRange(); + const criteria = `total_${this.type}`; + this.sortedContributors = Object.values(contributors) + .filter((contributor) => contributor[criteria] !== 0) + .sort((a, b) => a[criteria] > b[criteria] ? -1 : a[criteria] === b[criteria] ? 0 : 1) + .slice(0, 100); + }, + + async fetchGraphData() { + this.isLoading = true; + try { + let response; + do { + response = await GET(`${this.repoLink}/activity/contributors/data`); + if (response.status === 202) { + await sleep(1000); // wait for 1 second before retrying + } + } while (response.status === 202); + if (response.ok) { + const data = await response.json(); + const {total, ...rest} = data; + // below line might be deleted if we are sure go produces map always sorted by keys + total.weeks = Object.fromEntries(Object.entries(total.weeks).sort()); + + const weekValues = Object.values(total.weeks); + this.xAxisStart = weekValues[0].week; + this.xAxisEnd = firstStartDateAfterDate(new Date()); + const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd)); + total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks); + this.xAxisMin = this.xAxisStart; + this.xAxisMax = this.xAxisEnd; + this.contributorsStats = {}; + for (const [email, user] of Object.entries(rest)) { + user.weeks = fillEmptyStartDaysWithZeroes(startDays, user.weeks); + this.contributorsStats[email] = user; + } + this.sortContributors(); + this.totalStats = total; + this.errorText = ''; + } else { + this.errorText = response.statusText; + } + } catch (err) { + this.errorText = err.message; + } finally { + this.isLoading = false; + } + }, + + filterContributorWeeksByDateRange() { + const filteredData = {}; + const data = this.contributorsStats; + for (const key of Object.keys(data)) { + const user = data[key]; + user.total_commits = 0; + user.total_additions = 0; + user.total_deletions = 0; + user.max_contribution_type = 0; + const filteredWeeks = user.weeks.filter((week) => { + const oneWeek = 7 * 24 * 60 * 60 * 1000; + if (week.week >= this.xAxisMin - oneWeek && week.week <= this.xAxisMax + oneWeek) { + user.total_commits += week.commits; + user.total_additions += week.additions; + user.total_deletions += week.deletions; + if (week[this.type] > user.max_contribution_type) { + user.max_contribution_type = week[this.type]; + } + return true; + } + return false; + }); + // this line is required. See https://github.com/sahinakkaya/gitea/pull/3#discussion_r1396495722 + // for details. + user.max_contribution_type += 1; + + filteredData[key] = {...user, weeks: filteredWeeks}; + } + + return filteredData; + }, + + maxMainGraph() { + // This method calculates maximum value for Y value of the main graph. If the number + // of maximum contributions for selected contribution type is 15.955 it is probably + // better to round it up to 20.000.This method is responsible for doing that. + // Normally, chartjs handles this automatically, but it will resize the graph when you + // zoom, pan etc. I think resizing the graph makes it harder to compare things visually. + const maxValue = Math.max( + ...this.totalStats.weeks.map((o) => o[this.type]), + ); + const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); + if (coefficient % 1 === 0) return maxValue; + return (1 - (coefficient % 1)) * 10 ** exp + maxValue; + }, + + maxContributorGraph() { + // Similar to maxMainGraph method this method calculates maximum value for Y value + // for contributors' graph. If I let chartjs do this for me, it will choose different + // maxY value for each contributors' graph which again makes it harder to compare. + const maxValue = Math.max( + ...this.sortedContributors.map((c) => c.max_contribution_type), + ); + const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); + if (coefficient % 1 === 0) return maxValue; + return (1 - (coefficient % 1)) * 10 ** exp + maxValue; + }, + + toGraphData(data) { + return { + datasets: [ + { + data: data.map((i) => ({x: i.week, y: i[this.type]})), + pointRadius: 0, + pointHitRadius: 0, + fill: 'start', + backgroundColor: chartJsColors[this.type], + borderWidth: 0, + tension: 0.3, + }, + ], + }; + }, + + updateOtherCharts(event, reset) { + const minVal = event.chart.options.scales.x.min; + const maxVal = event.chart.options.scales.x.max; + if (reset) { + this.xAxisMin = this.xAxisStart; + this.xAxisMax = this.xAxisEnd; + this.sortContributors(); + } else if (minVal) { + this.xAxisMin = minVal; + this.xAxisMax = maxVal; + this.sortContributors(); + } + }, + + getOptions(type) { + return { + responsive: true, + maintainAspectRatio: false, + animation: false, + events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove', 'dblclick'], + plugins: { + title: { + display: type === 'main', + text: 'drag: zoom, shift+drag: pan, double click: reset zoom', + position: 'top', + align: 'center', + }, + customEventListener: { + chartType: type, + instance: this, + }, + zoom: { + pan: { + enabled: true, + modifierKey: 'shift', + mode: 'x', + threshold: 20, + onPanComplete: this.updateOtherCharts, + }, + limits: { + x: { + // Check https://www.chartjs.org/chartjs-plugin-zoom/latest/guide/options.html#scale-limits + // to know what each option means + min: 'original', + max: 'original', + + // number of milliseconds in 2 weeks. Minimum x range will be 2 weeks when you zoom on the graph + minRange: 2 * 7 * 24 * 60 * 60 * 1000, + }, + }, + zoom: { + drag: { + enabled: type === 'main', + }, + pinch: { + enabled: type === 'main', + }, + mode: 'x', + onZoomComplete: this.updateOtherCharts, + }, + }, + }, + scales: { + x: { + min: this.xAxisMin, + max: this.xAxisMax, + type: 'time', + grid: { + display: false, + }, + time: { + minUnit: 'month', + }, + ticks: { + maxRotation: 0, + maxTicksLimit: type === 'main' ? 12 : 6, + }, + }, + y: { + min: 0, + max: type === 'main' ? this.maxMainGraph() : this.maxContributorGraph(), + ticks: { + maxTicksLimit: type === 'main' ? 6 : 4, + }, + }, + }, + }; + }, + }, +}; +</script> +<template> + <div> + <div class="ui header tw-flex tw-items-center tw-justify-between"> + <div> + <relative-time + v-if="xAxisMin > 0" + format="datetime" + year="numeric" + month="short" + day="numeric" + weekday="" + :datetime="new Date(xAxisMin)" + > + {{ new Date(xAxisMin) }} + </relative-time> + {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "-" }} + <relative-time + v-if="xAxisMax > 0" + format="datetime" + year="numeric" + month="short" + day="numeric" + weekday="" + :datetime="new Date(xAxisMax)" + > + {{ new Date(xAxisMax) }} + </relative-time> + </div> + <div> + <!-- Contribution type --> + <div class="ui dropdown jump" id="repo-contributors"> + <div class="ui basic compact button"> + <span class="text"> + <span class="not-mobile">{{ locale.filterLabel }} </span><strong>{{ locale.contributionType[type] }}</strong> + <svg-icon name="octicon-triangle-down" :size="14"/> + </span> + </div> + <div class="menu"> + <div :class="['item', {'active': type === 'commits'}]"> + {{ locale.contributionType.commits }} + </div> + <div :class="['item', {'active': type === 'additions'}]"> + {{ locale.contributionType.additions }} + </div> + <div :class="['item', {'active': type === 'deletions'}]"> + {{ locale.contributionType.deletions }} + </div> + </div> + </div> + </div> + </div> + <div class="tw-flex ui segment main-graph"> + <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto"> + <div v-if="isLoading"> + <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/> + {{ locale.loadingInfo }} + </div> + <div v-else class="text red"> + <SvgIcon name="octicon-x-circle-fill"/> + {{ errorText }} + </div> + </div> + <ChartLine + v-memo="[totalStats.weeks, type]" v-if="Object.keys(totalStats).length !== 0" + :data="toGraphData(totalStats.weeks)" :options="getOptions('main')" + /> + </div> + <div class="contributor-grid"> + <div + v-for="(contributor, index) in sortedContributors" + :key="index" + v-memo="[sortedContributors, type]" + > + <div class="ui top attached header tw-flex tw-flex-1"> + <b class="ui right">#{{ index + 1 }}</b> + <a :href="contributor.home_link"> + <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link"> + </a> + <div class="tw-ml-2"> + <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a> + <h4 v-else class="contributor-name"> + {{ contributor.name }} + </h4> + <p class="tw-text-12 tw-flex tw-gap-1"> + <strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong> + <strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong> + <strong v-if="contributor.total_deletions" class="text red"> + {{ contributor.total_deletions.toLocaleString() }}--</strong> + </p> + </div> + </div> + <div class="ui attached segment"> + <div> + <ChartLine + :data="toGraphData(contributor.weeks)" + :options="getOptions('contributor')" + /> + </div> + </div> + </div> + </div> + </div> +</template> +<style scoped> +.main-graph { + height: 260px; + padding-top: 2px; +} + +.contributor-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.contributor-grid > * { + min-width: 0; +} + +@media (max-width: 991.98px) { + .contributor-grid { + grid-template-columns: repeat(1, 1fr); + } +} + +.contributor-name { + margin-bottom: 0; +} +</style> diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue new file mode 100644 index 0000000000..502af533da --- /dev/null +++ b/web_src/js/components/RepoRecentCommits.vue @@ -0,0 +1,149 @@ +<script> +import {SvgIcon} from '../svg.js'; +import { + Chart, + Tooltip, + BarElement, + LinearScale, + TimeScale, +} from 'chart.js'; +import {GET} from '../modules/fetch.js'; +import {Bar} from 'vue-chartjs'; +import { + startDaysBetween, + firstStartDateAfterDate, + fillEmptyStartDaysWithZeroes, +} from '../utils/time.js'; +import {chartJsColors} from '../utils/color.js'; +import {sleep} from '../utils.js'; +import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; + +const {pageData} = window.config; + +Chart.defaults.color = chartJsColors.text; +Chart.defaults.borderColor = chartJsColors.border; + +Chart.register( + TimeScale, + LinearScale, + BarElement, + Tooltip, +); + +export default { + components: {Bar, SvgIcon}, + props: { + locale: { + type: Object, + required: true, + }, + }, + data: () => ({ + isLoading: false, + errorText: '', + repoLink: pageData.repoLink || [], + data: [], + }), + mounted() { + this.fetchGraphData(); + }, + methods: { + async fetchGraphData() { + this.isLoading = true; + try { + let response; + do { + response = await GET(`${this.repoLink}/activity/recent-commits/data`); + if (response.status === 202) { + await sleep(1000); // wait for 1 second before retrying + } + } while (response.status === 202); + if (response.ok) { + const data = await response.json(); + const start = Object.values(data)[0].week; + const end = firstStartDateAfterDate(new Date()); + const startDays = startDaysBetween(new Date(start), new Date(end)); + this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); + this.errorText = ''; + } else { + this.errorText = response.statusText; + } + } catch (err) { + this.errorText = err.message; + } finally { + this.isLoading = false; + } + }, + + toGraphData(data) { + return { + datasets: [ + { + data: data.map((i) => ({x: i.week, y: i.commits})), + label: 'Commits', + backgroundColor: chartJsColors['commits'], + borderWidth: 0, + tension: 0.3, + }, + ], + }; + }, + + getOptions() { + return { + responsive: true, + maintainAspectRatio: false, + animation: true, + scales: { + x: { + type: 'time', + grid: { + display: false, + }, + time: { + minUnit: 'week', + }, + ticks: { + maxRotation: 0, + maxTicksLimit: 52, + }, + }, + y: { + ticks: { + maxTicksLimit: 6, + }, + }, + }, + }; + }, + }, +}; +</script> +<template> + <div> + <div class="ui header tw-flex tw-items-center tw-justify-between"> + {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }} + </div> + <div class="tw-flex ui segment main-graph"> + <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto"> + <div v-if="isLoading"> + <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/> + {{ locale.loadingInfo }} + </div> + <div v-else class="text red"> + <SvgIcon name="octicon-x-circle-fill"/> + {{ errorText }} + </div> + </div> + <Bar + v-memo="data" v-if="data.length !== 0" + :data="toGraphData(data)" :options="getOptions()" + /> + </div> + </div> +</template> +<style scoped> +.main-graph { + height: 250px; +} +</style> diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue index f6af7e447f..103cc525ad 100644 --- a/web_src/js/components/ScopedAccessTokenSelector.vue +++ b/web_src/js/components/ScopedAccessTokenSelector.vue @@ -39,7 +39,7 @@ const sfc = { 'repository', 'user'); return categories; - } + }, }, mounted() { @@ -68,7 +68,7 @@ const sfc = { } // no scopes selected, show validation error showElem(warningEl); - } + }, }, }; @@ -87,7 +87,7 @@ export function initScopedAccessTokenCategories() { </script> <template> - <div v-for="category in categories" :key="category" class="field gt-pl-2 gt-pb-2 access-token-category"> + <div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category"> <label class="category-label" :for="'access-token-scope-' + category"> {{ category }} </label> diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js index 044976ea7b..f388b1122e 100644 --- a/web_src/js/features/admin/common.js +++ b/web_src/js/features/admin/common.js @@ -1,141 +1,169 @@ import $ from 'jquery'; import {checkAppUrl} from '../common-global.js'; import {hideElem, showElem, toggleElem} from '../../utils/dom.js'; +import {POST} from '../../modules/fetch.js'; -const {csrfToken, appSubUrl} = window.config; +const {appSubUrl} = window.config; + +function onSecurityProtocolChange() { + if (Number(document.getElementById('security_protocol')?.value) > 0) { + showElem('.has-tls'); + } else { + hideElem('.has-tls'); + } +} export function initAdminCommon() { - if ($('.page-content.admin').length === 0) { - return; - } + if (!document.querySelector('.page-content.admin')) return; // check whether appUrl(ROOT_URL) is correct, if not, show an error message checkAppUrl(); // New user if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) { - $('#login_type').on('change', function () { - if ($(this).val().substring(0, 1) === '0') { - $('#user_name').removeAttr('disabled'); - $('#login_name').removeAttr('required'); - hideElem($('.non-local')); - showElem($('.local')); - $('#user_name').trigger('focus'); + document.getElementById('login_type')?.addEventListener('change', function () { + if (this.value?.substring(0, 1) === '0') { + document.getElementById('user_name')?.removeAttribute('disabled'); + document.getElementById('login_name')?.removeAttribute('required'); + hideElem('.non-local'); + showElem('.local'); + document.getElementById('user_name')?.focus(); - if ($(this).data('password') === 'required') { - $('#password').attr('required', 'required'); + if (this.getAttribute('data-password') === 'required') { + document.getElementById('password')?.setAttribute('required', 'required'); } } else { - if ($('.admin.edit.user').length > 0) { - $('#user_name').attr('disabled', 'disabled'); + if (document.querySelector('.admin.edit.user')) { + document.getElementById('user_name')?.setAttribute('disabled', 'disabled'); } - $('#login_name').attr('required', 'required'); - showElem($('.non-local')); - hideElem($('.local')); - $('#login_name').trigger('focus'); + document.getElementById('login_name')?.setAttribute('required', 'required'); + showElem('.non-local'); + hideElem('.local'); + document.getElementById('login_name')?.focus(); - $('#password').removeAttr('required'); + document.getElementById('password')?.removeAttribute('required'); } }); } - function onSecurityProtocolChange() { - if ($('#security_protocol').val() > 0) { - showElem($('.has-tls')); - } else { - hideElem($('.has-tls')); - } - } - function onUsePagedSearchChange() { - if ($('#use_paged_search').prop('checked')) { + const searchPageSizeElements = document.querySelectorAll('.search-page-size'); + if (document.getElementById('use_paged_search').checked) { showElem('.search-page-size'); - $('.search-page-size').find('input').attr('required', 'required'); + for (const el of searchPageSizeElements) { + el.querySelector('input')?.setAttribute('required', 'required'); + } } else { hideElem('.search-page-size'); - $('.search-page-size').find('input').removeAttr('required'); + for (const el of searchPageSizeElements) { + el.querySelector('input')?.removeAttribute('required'); + } } } function onOAuth2Change(applyDefaultValues) { - hideElem($('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url')); - $('.open_id_connect_auto_discovery_url input[required]').removeAttr('required'); + hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url'); + for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) { + input.removeAttribute('required'); + } - const provider = $('#oauth2_provider').val(); + const provider = document.getElementById('oauth2_provider')?.value; switch (provider) { case 'openidConnect': - $('.open_id_connect_auto_discovery_url input').attr('required', 'required'); - showElem($('.open_id_connect_auto_discovery_url')); + for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input')) { + input.setAttribute('required', 'required'); + } + showElem('.open_id_connect_auto_discovery_url'); break; default: - if ($(`#${provider}_customURLSettings`).data('required')) { - $('#oauth2_use_custom_url').attr('checked', 'checked'); + if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-required')) { + document.getElementById('oauth2_use_custom_url')?.setAttribute('checked', 'checked'); } - if ($(`#${provider}_customURLSettings`).data('available')) { - showElem($('.oauth2_use_custom_url')); + if (document.getElementById(`#${provider}_customURLSettings`)?.getAttribute('data-available')) { + showElem('.oauth2_use_custom_url'); } } onOAuth2UseCustomURLChange(applyDefaultValues); } function onOAuth2UseCustomURLChange(applyDefaultValues) { - const provider = $('#oauth2_provider').val(); - hideElem($('.oauth2_use_custom_url_field')); - $('.oauth2_use_custom_url_field input[required]').removeAttr('required'); + const provider = document.getElementById('oauth2_provider')?.value; + hideElem('.oauth2_use_custom_url_field'); + for (const input of document.querySelectorAll('.oauth2_use_custom_url_field input[required]')) { + input.removeAttribute('required'); + } - if ($('#oauth2_use_custom_url').is(':checked')) { + if (document.getElementById('oauth2_use_custom_url')?.checked) { for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) { if (applyDefaultValues) { - $(`#oauth2_${custom}`).val($(`#${provider}_${custom}`).val()); + document.getElementById(`oauth2_${custom}`).value = document.getElementById(`${provider}_${custom}`).value; } - if ($(`#${provider}_${custom}`).data('available')) { - $(`.oauth2_${custom} input`).attr('required', 'required'); - showElem($(`.oauth2_${custom}`)); + const customInput = document.getElementById(`${provider}_${custom}`); + if (customInput && customInput.getAttribute('data-available')) { + for (const input of document.querySelectorAll(`.oauth2_${custom} input`)) { + input.setAttribute('required', 'required'); + } + showElem(`.oauth2_${custom}`); } } } } function onEnableLdapGroupsChange() { - toggleElem($('#ldap-group-options'), $('.js-ldap-group-toggle').is(':checked')); + toggleElem(document.getElementById('ldap-group-options'), $('.js-ldap-group-toggle')[0].checked); } // New authentication - if ($('.admin.new.authentication').length > 0) { - $('#auth_type').on('change', function () { - hideElem($('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi')); + if (document.querySelector('.admin.new.authentication')) { + document.getElementById('auth_type')?.addEventListener('change', function () { + hideElem('.ldap, .dldap, .smtp, .pam, .oauth2, .has-tls, .search-page-size, .sspi'); - $('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]').removeAttr('required'); - $('.binddnrequired').removeClass('required'); + for (const input of document.querySelectorAll('.ldap input[required], .binddnrequired input[required], .dldap input[required], .smtp input[required], .pam input[required], .oauth2 input[required], .has-tls input[required], .sspi input[required]')) { + input.removeAttribute('required'); + } - const authType = $(this).val(); + document.querySelector('.binddnrequired')?.classList.remove('required'); + + const authType = this.value; switch (authType) { case '2': // LDAP - showElem($('.ldap')); - $('.binddnrequired input, .ldap div.required:not(.dldap) input').attr('required', 'required'); - $('.binddnrequired').addClass('required'); + showElem('.ldap'); + for (const input of document.querySelectorAll('.binddnrequired input, .ldap div.required:not(.dldap) input')) { + input.setAttribute('required', 'required'); + } + document.querySelector('.binddnrequired')?.classList.add('required'); break; case '3': // SMTP - showElem($('.smtp')); - showElem($('.has-tls')); - $('.smtp div.required input, .has-tls').attr('required', 'required'); + showElem('.smtp'); + showElem('.has-tls'); + for (const input of document.querySelectorAll('.smtp div.required input, .has-tls')) { + input.setAttribute('required', 'required'); + } break; case '4': // PAM - showElem($('.pam')); - $('.pam input').attr('required', 'required'); + showElem('.pam'); + for (const input of document.querySelectorAll('.pam input')) { + input.setAttribute('required', 'required'); + } break; case '5': // LDAP - showElem($('.dldap')); - $('.dldap div.required:not(.ldap) input').attr('required', 'required'); + showElem('.dldap'); + for (const input of document.querySelectorAll('.dldap div.required:not(.ldap) input')) { + input.setAttribute('required', 'required'); + } break; case '6': // OAuth2 - showElem($('.oauth2')); - $('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input').attr('required', 'required'); + showElem('.oauth2'); + for (const input of document.querySelectorAll('.oauth2 div.required:not(.oauth2_use_custom_url,.oauth2_use_custom_url_field,.open_id_connect_auto_discovery_url) input')) { + input.setAttribute('required', 'required'); + } onOAuth2Change(true); break; case '7': // SSPI - showElem($('.sspi')); - $('.sspi div.required input').attr('required', 'required'); + showElem('.sspi'); + for (const input of document.querySelectorAll('.sspi div.required input')) { + input.setAttribute('required', 'required'); + } break; } if (authType === '2' || authType === '5') { @@ -147,79 +175,81 @@ export function initAdminCommon() { } }); $('#auth_type').trigger('change'); - $('#security_protocol').on('change', onSecurityProtocolChange); - $('#use_paged_search').on('change', onUsePagedSearchChange); - $('#oauth2_provider').on('change', () => onOAuth2Change(true)); - $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(true)); + document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange); + document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange); + document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true)); + document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(true)); $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange); } // Edit authentication - if ($('.admin.edit.authentication').length > 0) { - const authType = $('#auth_type').val(); + if (document.querySelector('.admin.edit.authentication')) { + const authType = document.getElementById('auth_type')?.value; if (authType === '2' || authType === '5') { - $('#security_protocol').on('change', onSecurityProtocolChange); + document.getElementById('security_protocol')?.addEventListener('change', onSecurityProtocolChange); $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange); onEnableLdapGroupsChange(); if (authType === '2') { - $('#use_paged_search').on('change', onUsePagedSearchChange); + document.getElementById('use_paged_search')?.addEventListener('change', onUsePagedSearchChange); } } else if (authType === '6') { - $('#oauth2_provider').on('change', () => onOAuth2Change(true)); - $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(false)); + document.getElementById('oauth2_provider')?.addEventListener('change', () => onOAuth2Change(true)); + document.getElementById('oauth2_use_custom_url')?.addEventListener('change', () => onOAuth2UseCustomURLChange(false)); onOAuth2Change(false); } } - if ($('.admin.authentication').length > 0) { + if (document.querySelector('.admin.authentication')) { $('#auth_name').on('input', function () { // appSubUrl is either empty or is a path that starts with `/` and doesn't have a trailing slash. - $('#oauth2-callback-url').text(`${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent($(this).val())}/callback`); + document.getElementById('oauth2-callback-url').textContent = `${window.location.origin}${appSubUrl}/user/oauth2/${encodeURIComponent(this.value)}/callback`; }).trigger('input'); } // Notice - if ($('.admin.notice')) { - const $detailModal = $('#detail-modal'); + if (document.querySelector('.admin.notice')) { + const $detailModal = document.getElementById('detail-modal'); // Attach view detail modals $('.view-detail').on('click', function () { $detailModal.find('.content pre').text($(this).parents('tr').find('.notice-description').text()); - $detailModal.find('.sub.header').text($(this).parents('tr').find('relative-time').attr('title')); + $detailModal.find('.sub.header').text(this.closest('tr')?.querySelector('relative-time')?.getAttribute('title')); $detailModal.modal('show'); return false; }); // Select actions - const $checkboxes = $('.select.table .ui.checkbox'); + const checkboxes = document.querySelectorAll('.select.table .ui.checkbox input'); + $('.select.action').on('click', function () { switch ($(this).data('action')) { case 'select-all': - $checkboxes.checkbox('check'); + for (const checkbox of checkboxes) { + checkbox.checked = true; + } break; case 'deselect-all': - $checkboxes.checkbox('uncheck'); + for (const checkbox of checkboxes) { + checkbox.checked = false; + } break; case 'inverse': - $checkboxes.checkbox('toggle'); + for (const checkbox of checkboxes) { + checkbox.checked = !checkbox.checked; + } break; } }); - $('#delete-selection').on('click', function (e) { + document.getElementById('delete-selection')?.addEventListener('click', async function (e) { e.preventDefault(); - const $this = $(this); - $this.addClass('loading disabled'); - const ids = []; - $checkboxes.each(function () { - if ($(this).checkbox('is checked')) { - ids.push($(this).data('id')); + this.classList.add('is-loading', 'disabled'); + const data = new FormData(); + for (const checkbox of checkboxes) { + if (checkbox.checked) { + data.append('ids[]', checkbox.closest('.ui.checkbox').getAttribute('data-id')); } - }); - $.post($this.data('link'), { - _csrf: csrfToken, - ids - }).done(() => { - window.location.href = $this.data('redirect'); - }); + } + await POST(this.getAttribute('data-link'), {data}); + window.location.href = this.getAttribute('data-redirect'); }); } } diff --git a/web_src/js/features/admin/users.js b/web_src/js/features/admin/users.js index c8edaab549..7cac603b5c 100644 --- a/web_src/js/features/admin/users.js +++ b/web_src/js/features/admin/users.js @@ -1,34 +1,39 @@ -import $ from 'jquery'; - export function initAdminUserListSearchForm() { const searchForm = window.config.pageData.adminUserListSearchForm; if (!searchForm) return; - const $form = $('#user-list-search-form'); - if (!$form.length) return; + const form = document.querySelector('#user-list-search-form'); + if (!form) return; - $form.find(`button[name=sort][value=${searchForm.SortType}]`).addClass('active'); + for (const button of form.querySelectorAll(`button[name=sort][value="${searchForm.SortType}"]`)) { + button.classList.add('active'); + } if (searchForm.StatusFilterMap) { for (const [k, v] of Object.entries(searchForm.StatusFilterMap)) { if (!v) continue; - $form.find(`input[name="status_filter[${k}]"][value=${v}]`).prop('checked', true); + for (const input of form.querySelectorAll(`input[name="status_filter[${k}]"][value="${v}"]`)) { + input.checked = true; + } } } - $form.find(`input[type=radio]`).on('click', () => { - $form.trigger('submit'); - return false; - }); - - $form.find('.j-reset-status-filter').on('click', () => { - $form.find(`input[type=radio]`).each((_, e) => { - const $e = $(e); - if ($e.attr('name').startsWith('status_filter[')) { - $e.prop('checked', false); - } + for (const radio of form.querySelectorAll('input[type=radio]')) { + radio.addEventListener('click', () => { + form.submit(); }); - $form.trigger('submit'); - return false; - }); + } + + const resetButtons = form.querySelectorAll('.j-reset-status-filter'); + for (const button of resetButtons) { + button.addEventListener('click', (e) => { + e.preventDefault(); + for (const input of form.querySelectorAll('input[type=radio]')) { + if (input.name.startsWith('status_filter[')) { + input.checked = false; + } + } + form.submit(); + }); + } } diff --git a/web_src/js/features/autofocus-end.js b/web_src/js/features/autofocus-end.js new file mode 100644 index 0000000000..da71ce9536 --- /dev/null +++ b/web_src/js/features/autofocus-end.js @@ -0,0 +1,6 @@ +export function initAutoFocusEnd() { + for (const el of document.querySelectorAll('.js-autofocus-end')) { + el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus. + el.setSelectionRange(el.value.length, el.value.length); + } +} diff --git a/web_src/js/features/captcha.js b/web_src/js/features/captcha.js index 3da5dbda41..c803a5006b 100644 --- a/web_src/js/features/captcha.js +++ b/web_src/js/features/captcha.js @@ -9,7 +9,7 @@ export async function initCaptcha() { const params = { sitekey: siteKey, - theme: isDark ? 'dark' : 'light' + theme: isDark ? 'dark' : 'light', }; switch (captchaEl.getAttribute('data-captcha-type')) { @@ -42,7 +42,7 @@ export async function initCaptcha() { siteKey: { instanceUrl: new URL(instanceURL), key: siteKey, - } + }, }); break; } diff --git a/web_src/js/features/citation.js b/web_src/js/features/citation.js index 61f378f0f2..918a467136 100644 --- a/web_src/js/features/citation.js +++ b/web_src/js/features/citation.js @@ -1,8 +1,9 @@ import $ from 'jquery'; +import {getCurrentLocale} from '../utils.js'; const {pageData} = window.config; -async function initInputCitationValue($citationCopyApa, $citationCopyBibtex) { +async function initInputCitationValue(citationCopyApa, citationCopyBibtex) { const [{Cite, plugins}] = await Promise.all([ import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'), @@ -14,11 +15,11 @@ async function initInputCitationValue($citationCopyApa, $citationCopyBibtex) { config.constants.fieldTypes.doi = ['field', 'literal']; config.constants.fieldTypes.version = ['field', 'literal']; const citationFormatter = new Cite(citationFileContent); - const lang = document.documentElement.lang || 'en-US'; + const lang = getCurrentLocale() || 'en-US'; const apaOutput = citationFormatter.format('bibliography', {template: 'apa', lang}); const bibtexOutput = citationFormatter.format('bibtex', {lang}); - $citationCopyBibtex.attr('data-text', bibtexOutput); - $citationCopyApa.attr('data-text', apaOutput); + citationCopyBibtex.setAttribute('data-text', bibtexOutput); + citationCopyApa.setAttribute('data-text', apaOutput); } export async function initCitationFileCopyContent() { @@ -26,42 +27,50 @@ export async function initCitationFileCopyContent() { if (!pageData.citationFileContent) return; - const $citationCopyApa = $('#citation-copy-apa'); - const $citationCopyBibtex = $('#citation-copy-bibtex'); - const $inputContent = $('#citation-copy-content'); + const citationCopyApa = document.getElementById('citation-copy-apa'); + const citationCopyBibtex = document.getElementById('citation-copy-bibtex'); + const inputContent = document.getElementById('citation-copy-content'); + + if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return; - if ((!$citationCopyApa.length && !$citationCopyBibtex.length) || !$inputContent.length) return; const updateUi = () => { const isBibtex = (localStorage.getItem('citation-copy-format') || defaultCitationFormat) === 'bibtex'; - const copyContent = (isBibtex ? $citationCopyBibtex : $citationCopyApa).attr('data-text'); - - $inputContent.val(copyContent); - $citationCopyBibtex.toggleClass('primary', isBibtex); - $citationCopyApa.toggleClass('primary', !isBibtex); + const copyContent = (isBibtex ? citationCopyBibtex : citationCopyApa).getAttribute('data-text'); + inputContent.value = copyContent; + citationCopyBibtex.classList.toggle('primary', isBibtex); + citationCopyApa.classList.toggle('primary', !isBibtex); }; - try { - await initInputCitationValue($citationCopyApa, $citationCopyBibtex); - } catch (e) { - console.error(`initCitationFileCopyContent error: ${e}`, e); - return; - } - updateUi(); + document.getElementById('cite-repo-button')?.addEventListener('click', async (e) => { + const dropdownBtn = e.target.closest('.ui.dropdown.button'); + dropdownBtn.classList.add('is-loading'); - $citationCopyApa.on('click', () => { - localStorage.setItem('citation-copy-format', 'apa'); - updateUi(); - }); - $citationCopyBibtex.on('click', () => { - localStorage.setItem('citation-copy-format', 'bibtex'); - updateUi(); - }); + try { + try { + await initInputCitationValue(citationCopyApa, citationCopyBibtex); + } catch (e) { + console.error(`initCitationFileCopyContent error: ${e}`, e); + return; + } + updateUi(); - $inputContent.on('click', () => { - $inputContent.trigger('select'); - }); + citationCopyApa.addEventListener('click', () => { + localStorage.setItem('citation-copy-format', 'apa'); + updateUi(); + }); + + citationCopyBibtex.addEventListener('click', () => { + localStorage.setItem('citation-copy-format', 'bibtex'); + updateUi(); + }); + + inputContent.addEventListener('click', () => { + inputContent.select(); + }); + } finally { + dropdownBtn.classList.remove('is-loading'); + } - $('#cite-repo-button').on('click', () => { $('#cite-repo-modal').modal('show'); }); } diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index 224628658e..daf7e2ae2d 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -5,37 +5,28 @@ import {clippie} from 'clippie'; const {copy_success, copy_error} = window.config.i18n; // Enable clipboard copy from HTML attributes. These properties are supported: -// - data-clipboard-text: Direct text to copy, has highest precedence +// - data-clipboard-text: Direct text to copy // - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied // - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls export function initGlobalCopyToClipboardListener() { - document.addEventListener('click', (e) => { - let target = e.target; - // In case <button data-clipboard-text><svg></button>, so we just search - // up to 3 levels for performance - for (let i = 0; i < 3 && target; i++) { - let text = target.getAttribute('data-clipboard-text'); + document.addEventListener('click', async (e) => { + const target = e.target.closest('[data-clipboard-text], [data-clipboard-target]'); + if (!target) return; - if (!text && target.getAttribute('data-clipboard-target')) { - text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value; - } + e.preventDefault(); - if (text && target.getAttribute('data-clipboard-text-type') === 'url') { - text = toAbsoluteUrl(text); - } + let text = target.getAttribute('data-clipboard-text'); + if (!text) { + text = document.querySelector(target.getAttribute('data-clipboard-target'))?.value; + } - if (text) { - e.preventDefault(); + if (text && target.getAttribute('data-clipboard-text-type') === 'url') { + text = toAbsoluteUrl(text); + } - (async() => { - const success = await clippie(text); - showTemporaryTooltip(target, success ? copy_success : copy_error); - })(); - - break; - } - - target = target.parentElement; + if (text) { + const success = await clippie(text); + showTemporaryTooltip(target, success ? copy_success : copy_error); } }); } diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js new file mode 100644 index 0000000000..47e1539ddc --- /dev/null +++ b/web_src/js/features/code-frequency.js @@ -0,0 +1,21 @@ +import {createApp} from 'vue'; + +export async function initRepoCodeFrequency() { + const el = document.getElementById('repo-code-frequency-chart'); + if (!el) return; + + const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue'); + try { + const View = createApp(RepoCodeFrequency, { + locale: { + loadingTitle: el.getAttribute('data-locale-loading-title'), + loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'), + loadingInfo: el.getAttribute('data-locale-loading-info'), + }, + }); + View.mount(el); + } catch (err) { + console.error('RepoCodeFrequency failed to load', err); + el.textContent = el.getAttribute('data-locale-component-failed-to-load'); + } +} diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js index fceb2f7620..4fb8bb9e63 100644 --- a/web_src/js/features/codeeditor.js +++ b/web_src/js/features/codeeditor.js @@ -80,7 +80,7 @@ export async function createMonaco(textarea, filename, editorOpts) { rules: [ { background: getColor('--color-code-bg'), - } + }, ], colors: { 'editor.background': getColor('--color-code-bg'), @@ -98,7 +98,7 @@ export async function createMonaco(textarea, filename, editorOpts) { 'input.foreground': getColor('--color-input-text'), 'scrollbar.shadow': getColor('--color-shadow'), 'progressBar.background': getColor('--color-primary'), - } + }, }); // Quick fix: https://github.com/microsoft/monaco-editor/issues/2962 diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js index a5fdb3f5a6..6d00d908c9 100644 --- a/web_src/js/features/colorpicker.js +++ b/web_src/js/features/colorpicker.js @@ -1,10 +1,66 @@ -export async function createColorPicker($els) { - if (!$els || !$els.length) return; +import {createTippy} from '../modules/tippy.js'; + +export async function initColorPickers() { + const els = document.getElementsByClassName('js-color-picker-input'); + if (!els.length) return; await Promise.all([ - import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors'), - import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors/jquery.minicolors.css'), + import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'), + import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'), ]); - $els.minicolors(); + for (const el of els) { + initPicker(el); + } +} + +function updateSquare(el, newValue) { + el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent'; +} + +function updatePicker(el, newValue) { + el.setAttribute('color', newValue); +} + +function initPicker(el) { + const input = el.querySelector('input'); + + const square = document.createElement('div'); + square.classList.add('preview-square'); + updateSquare(square, input.value); + el.append(square); + + const picker = document.createElement('hex-color-picker'); + picker.addEventListener('color-changed', (e) => { + input.value = e.detail.value; + input.focus(); + updateSquare(square, e.detail.value); + }); + + input.addEventListener('input', (e) => { + updateSquare(square, e.target.value); + updatePicker(picker, e.target.value); + }); + + createTippy(input, { + trigger: 'focus click', + theme: 'bare', + hideOnClick: true, + content: picker, + placement: 'bottom-start', + interactive: true, + onShow() { + updatePicker(picker, input.value); + }, + }); + + // init precolors + for (const colorEl of el.querySelectorAll('.precolors .color')) { + colorEl.addEventListener('click', (e) => { + const newValue = e.target.getAttribute('data-color-hex'); + input.value = newValue; + input.dispatchEvent(new Event('input', {bubbles: true})); + updateSquare(square, newValue); + }); + } } diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index e8b546970f..e7db9b2336 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -2,7 +2,6 @@ import $ from 'jquery'; import '../vendor/jquery.are-you-sure.js'; import {clippie} from 'clippie'; import {createDropzone} from './dropzone.js'; -import {initCompColorPicker} from './comp/ColorPicker.js'; import {showGlobalErrorMessage} from '../bootstrap.js'; import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js'; import {svg} from '../svg.js'; @@ -11,7 +10,7 @@ import {htmlEscape} from 'escape-goat'; import {showTemporaryTooltip} from '../modules/tippy.js'; import {confirmModal} from './comp/ConfirmModal.js'; import {showErrorToast} from '../modules/toast.js'; -import {request, POST} from '../modules/fetch.js'; +import {request, POST, GET} from '../modules/fetch.js'; import '../htmx.js'; const {appUrl, appSubUrl, csrfToken, i18n} = window.config; @@ -19,7 +18,7 @@ const {appUrl, appSubUrl, csrfToken, i18n} = window.config; export function initGlobalFormDirtyLeaveConfirm() { // Warn users that try to leave a page after entering data into a form. // Except on sign-in pages, and for forms marked as 'ignore-dirty'. - if ($('.user.signin').length === 0) { + if (!$('.user.signin').length) { $('form:not(.ignore-dirty)').areYouSure(); } } @@ -37,11 +36,10 @@ export function initHeadNavbarContentToggle() { } export function initFootLanguageMenu() { - function linkLanguageAction() { + async function linkLanguageAction() { const $this = $(this); - $.get($this.data('url')).always(() => { - window.location.reload(); - }); + await GET($this.data('url')); + window.location.reload(); } $('.language-menu a[lang]').on('click', linkLanguageAction); @@ -92,19 +90,26 @@ async function fetchActionDoRequest(actionElem, url, opt) { } else { window.location.reload(); } + return; } else if (resp.status >= 400 && resp.status < 500) { const data = await resp.json(); // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. - showErrorToast(data.errorMessage || `server error: ${resp.status}`); + if (data.errorMessage) { + showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'}); + } else { + showErrorToast(`server error: ${resp.status}`); + } } else { showErrorToast(`server error: ${resp.status}`); } } catch (e) { - console.error('error when doRequest', e); - actionElem.classList.remove('is-loading', 'small-loading-icon'); - showErrorToast(i18n.network_error); + if (e.name !== 'AbortError') { + console.error('error when doRequest', e); + showErrorToast(`${i18n.network_error} ${e}`); + } } + actionElem.classList.remove('is-loading', 'loading-icon-2px'); } async function formFetchAction(e) { @@ -116,7 +121,7 @@ async function formFetchAction(e) { formEl.classList.add('is-loading'); if (formEl.clientHeight < 50) { - formEl.classList.add('small-loading-icon'); + formEl.classList.add('loading-icon-2px'); } const formMethod = formEl.getAttribute('method') || 'get'; @@ -190,8 +195,6 @@ export function initGlobalCommon() { $uiDropdowns.filter('.upward').dropdown('setting', 'direction', 'upward'); $uiDropdowns.filter('.downward').dropdown('setting', 'direction', 'downward'); - $('.ui.checkbox').checkbox(); - $('.tabular.menu .item').tab(); initSubmitEventPolyfill(); @@ -200,65 +203,68 @@ export function initGlobalCommon() { } export function initGlobalDropzone() { - // Dropzone for (const el of document.querySelectorAll('.dropzone')) { - const $dropzone = $(el); - const _promise = createDropzone(el, { - url: $dropzone.data('upload-url'), - headers: {'X-Csrf-Token': csrfToken}, - maxFiles: $dropzone.data('max-file'), - maxFilesize: $dropzone.data('max-size'), - acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'), - addRemoveLinks: true, - dictDefaultMessage: $dropzone.data('default-message'), - dictInvalidFileType: $dropzone.data('invalid-input-type'), - dictFileTooBig: $dropzone.data('file-too-big'), - dictRemoveFile: $dropzone.data('remove-file'), - timeout: 0, - thumbnailMethod: 'contain', - thumbnailWidth: 480, - thumbnailHeight: 480, - init() { - this.on('success', (file, data) => { - file.uuid = data.uuid; - const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); - $dropzone.find('.files').append(input); - // Create a "Copy Link" element, to conveniently copy the image - // or file link as Markdown to the clipboard - const copyLinkElement = document.createElement('div'); - copyLinkElement.className = 'gt-text-center'; - // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone - copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`; - copyLinkElement.addEventListener('click', async (e) => { - e.preventDefault(); - let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; - if (file.type.startsWith('image/')) { - fileMarkdown = `!${fileMarkdown}`; - } else if (file.type.startsWith('video/')) { - fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`; - } - const success = await clippie(fileMarkdown); - showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); - }); - file.previewTemplate.append(copyLinkElement); - }); - this.on('removedfile', (file) => { - $(`#${file.uuid}`).remove(); - if ($dropzone.data('remove-url')) { - POST($dropzone.data('remove-url'), { - data: new URLSearchParams({file: file.uuid}), - }); - } - }); - this.on('error', function (file, message) { - showErrorToast(message); - this.removeFile(file); - }); - }, - }); + initDropzone(el); } } +export function initDropzone(el) { + const $dropzone = $(el); + const _promise = createDropzone(el, { + url: $dropzone.data('upload-url'), + headers: {'X-Csrf-Token': csrfToken}, + maxFiles: $dropzone.data('max-file'), + maxFilesize: $dropzone.data('max-size'), + acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'), + addRemoveLinks: true, + dictDefaultMessage: $dropzone.data('default-message'), + dictInvalidFileType: $dropzone.data('invalid-input-type'), + dictFileTooBig: $dropzone.data('file-too-big'), + dictRemoveFile: $dropzone.data('remove-file'), + timeout: 0, + thumbnailMethod: 'contain', + thumbnailWidth: 480, + thumbnailHeight: 480, + init() { + this.on('success', (file, data) => { + file.uuid = data.uuid; + const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); + $dropzone.find('.files').append($input); + // Create a "Copy Link" element, to conveniently copy the image + // or file link as Markdown to the clipboard + const copyLinkElement = document.createElement('div'); + copyLinkElement.className = 'tw-text-center'; + // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone + copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`; + copyLinkElement.addEventListener('click', async (e) => { + e.preventDefault(); + let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; + if (file.type.startsWith('image/')) { + fileMarkdown = `!${fileMarkdown}`; + } else if (file.type.startsWith('video/')) { + fileMarkdown = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`; + } + const success = await clippie(fileMarkdown); + showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); + }); + file.previewTemplate.append(copyLinkElement); + }); + this.on('removedfile', (file) => { + $(`#${file.uuid}`).remove(); + if ($dropzone.data('remove-url')) { + POST($dropzone.data('remove-url'), { + data: new URLSearchParams({file: file.uuid}), + }); + } + }); + this.on('error', function (file, message) { + showErrorToast(message); + this.removeFile(file); + }); + }, + }); +} + async function linkAction(e) { // A "link-action" can post AJAX request to its "data-url" // Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading. @@ -292,42 +298,41 @@ export function initGlobalLinkActions() { const $this = $(this); const dataArray = $this.data(); let filter = ''; - if ($this.attr('data-modal-id')) { - filter += `#${$this.attr('data-modal-id')}`; + if (this.getAttribute('data-modal-id')) { + filter += `#${this.getAttribute('data-modal-id')}`; } - const dialog = $(`.delete.modal${filter}`); - dialog.find('.name').text($this.data('name')); + const $dialog = $(`.delete.modal${filter}`); + $dialog.find('.name').text($this.data('name')); for (const [key, value] of Object.entries(dataArray)) { if (key && key.startsWith('data')) { - dialog.find(`.${key}`).text(value); + $dialog.find(`.${key}`).text(value); } } - dialog.modal({ + $dialog.modal({ closable: false, - onApprove() { + onApprove: async () => { if ($this.data('type') === 'form') { $($this.data('form')).trigger('submit'); return; } - - const postData = { - _csrf: csrfToken, - }; + const postData = new FormData(); for (const [key, value] of Object.entries(dataArray)) { if (key && key.startsWith('data')) { - postData[key.slice(4)] = value; + postData.append(key.slice(4), value); } if (key === 'id') { - postData['id'] = value; + postData.append('id', value); } } - $.post($this.data('url'), postData).done((data) => { + const response = await POST($this.data('url'), {data: postData}); + if (response.ok) { + const data = await response.json(); window.location.href = data.redirect; - }); - } + } + }, }).modal('show'); } @@ -344,8 +349,7 @@ function initGlobalShowModal() { // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set. $('.show-modal').on('click', function (e) { e.preventDefault(); - const $el = $(this); - const modalSelector = $el.attr('data-modal'); + const modalSelector = this.getAttribute('data-modal'); const $modal = $(modalSelector); if (!$modal.length) { throw new Error('no modal for this action'); @@ -366,16 +370,13 @@ function initGlobalShowModal() { if (attrTargetAttr) { $attrTarget[0][attrTargetAttr] = attrib.value; - } else if ($attrTarget.is('input') || $attrTarget.is('textarea')) { + } else if ($attrTarget[0].matches('input, textarea')) { $attrTarget.val(attrib.value); // FIXME: add more supports like checkbox } else { $attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p } } - const colorPickers = $modal.find('.color-picker'); - if (colorPickers.length > 0) { - initCompColorPicker(); // FIXME: this might cause duplicate init - } + $modal.modal('setting', { onApprove: () => { // "form-fetch-action" can handle network errors gracefully, @@ -398,7 +399,7 @@ export function initGlobalButtons() { // a '.show-panel' element can show a panel, by `data-panel="selector"` // if it has "toggle" class, it toggles the panel e.preventDefault(); - const sel = $(this).attr('data-panel'); + const sel = this.getAttribute('data-panel'); if (this.classList.contains('toggle')) { toggleElem(sel); } else { @@ -409,12 +410,12 @@ export function initGlobalButtons() { $('.hide-panel').on('click', function (e) { // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"` e.preventDefault(); - let sel = $(this).attr('data-panel'); + let sel = this.getAttribute('data-panel'); if (sel) { hideElem($(sel)); return; } - sel = $(this).attr('data-panel-closest'); + sel = this.getAttribute('data-panel-closest'); if (sel) { hideElem($(this).closest(sel)); return; diff --git a/web_src/js/features/common-issue-list.js b/web_src/js/features/common-issue-list.js index 317c11219b..0c0f6c563d 100644 --- a/web_src/js/features/common-issue-list.js +++ b/web_src/js/features/common-issue-list.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.js'; import {GET} from '../modules/fetch.js'; @@ -30,42 +29,40 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) { } export function initCommonIssueListQuickGoto() { - const $goto = $('#issue-list-quick-goto'); - if (!$goto.length) return; + const goto = document.getElementById('issue-list-quick-goto'); + if (!goto) return; - const $form = $goto.closest('form'); - const $input = $form.find('input[name=q]'); - const repoLink = $goto.attr('data-repo-link'); + const form = goto.closest('form'); + const input = form.querySelector('input[name=q]'); + const repoLink = goto.getAttribute('data-repo-link'); - $form.on('submit', (e) => { + form.addEventListener('submit', (e) => { // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly - let doQuickGoto = !isElemHidden($goto); - const submitter = submitEventSubmitter(e.originalEvent); - if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false; + let doQuickGoto = !isElemHidden(goto); + const submitter = submitEventSubmitter(e); + if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false; if (!doQuickGoto) return; // if there is a goto button, use its link e.preventDefault(); - window.location.href = $goto.attr('data-issue-goto-link'); + window.location.href = goto.getAttribute('data-issue-goto-link'); }); const onInput = async () => { - const searchText = $input.val(); - + const searchText = input.value; // try to check whether the parsed goto link is valid let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText); if (targetUrl) { const res = await GET(`${targetUrl}/info`); if (res.status !== 200) targetUrl = ''; } - // if the input value has changed, then ignore the result - if ($input.val() !== searchText) return; + if (input.value !== searchText) return; - toggleElem($goto, Boolean(targetUrl)); - $goto.attr('data-issue-goto-link', targetUrl); + toggleElem(goto, Boolean(targetUrl)); + goto.setAttribute('data-issue-goto-link', targetUrl); }; - $input.on('input', onInputDebounce(onInput)); + input.addEventListener('input', onInputDebounce(onInput)); onInput(); } diff --git a/web_src/js/features/common-organization.js b/web_src/js/features/common-organization.js index 352e824b05..442714a3d6 100644 --- a/web_src/js/features/common-organization.js +++ b/web_src/js/features/common-organization.js @@ -1,14 +1,13 @@ -import $ from 'jquery'; import {initCompLabelEdit} from './comp/LabelEdit.js'; import {toggleElem} from '../utils/dom.js'; export function initCommonOrganization() { - if ($('.organization').length === 0) { + if (!document.querySelectorAll('.organization').length) { return; } - $('.organization.settings.options #org_name').on('input', function () { - const nameChanged = $(this).val().toLowerCase() !== $(this).attr('data-org-name').toLowerCase(); + document.querySelector('.organization.settings.options #org_name')?.addEventListener('input', function () { + const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase(); toggleElem('#org-name-change-prompt', nameChanged); }); diff --git a/web_src/js/features/comp/ColorPicker.js b/web_src/js/features/comp/ColorPicker.js deleted file mode 100644 index 5665b7a24a..0000000000 --- a/web_src/js/features/comp/ColorPicker.js +++ /dev/null @@ -1,12 +0,0 @@ -import $ from 'jquery'; -import {createColorPicker} from '../colorpicker.js'; - -export function initCompColorPicker() { - createColorPicker($('.color-picker')); - - $('.precolors .color').on('click', function () { - const color_hex = $(this).data('color-hex'); - $('.color-picker').val(color_hex); - $('.minicolors-swatch-color').css('background-color', color_hex); - }); -} diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index d486c5830a..d3fab375a9 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -2,29 +2,30 @@ import '@github/markdown-toolbar-element'; import '@github/text-expander-element'; import $ from 'jquery'; import {attachTribute} from '../tribute.js'; -import {hideElem, showElem, autosize} from '../../utils/dom.js'; -import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js'; +import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js'; +import {initEasyMDEPaste, initTextareaPaste} from './Paste.js'; import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; import {renderPreviewPanelContent} from '../repo-editor.js'; import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js'; import {initTextExpander} from './TextExpander.js'; import {showErrorToast} from '../../modules/toast.js'; +import {POST} from '../../modules/fetch.js'; let elementIdCounter = 0; /** * validate if the given textarea is non-empty. - * @param {jQuery} $textarea + * @param {HTMLElement} textarea - The textarea element to be validated. * @returns {boolean} returns true if validation succeeded. */ -export function validateTextareaNonEmpty($textarea) { +export function validateTextareaNonEmpty(textarea) { // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation. // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert. - if (!$textarea.val()) { - if ($textarea.is(':visible')) { - $textarea.prop('required', true); - const $form = $textarea.parents('form'); - $form[0]?.reportValidity(); + if (!textarea.value) { + if (isElemVisible(textarea)) { + textarea.required = true; + const form = textarea.closest('form'); + form?.reportValidity(); } else { // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places. showErrorToast('Require non-empty content'); @@ -83,6 +84,17 @@ class ComboMarkdownEditor { if (el.nodeName === 'BUTTON' && !el.getAttribute('type')) el.setAttribute('type', 'button'); } + this.textarea.addEventListener('keydown', (e) => { + if (e.shiftKey) { + e.target._shiftDown = true; + } + }); + this.textarea.addEventListener('keyup', (e) => { + if (!e.shiftKey) { + e.target._shiftDown = false; + } + }); + const monospaceButton = this.container.querySelector('.markdown-switch-monospace'); const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true'; const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text'); @@ -93,7 +105,7 @@ class ComboMarkdownEditor { e.preventDefault(); const enabled = localStorage?.getItem('markdown-editor-monospace') !== 'true'; localStorage.setItem('markdown-editor-monospace', String(enabled)); - this.textarea.classList.toggle('gt-mono', enabled); + this.textarea.classList.toggle('tw-font-mono', enabled); const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text'); monospaceButton.setAttribute('data-tooltip-content', text); monospaceButton.setAttribute('aria-checked', String(enabled)); @@ -107,7 +119,7 @@ class ComboMarkdownEditor { }); if (this.dropzone) { - initTextareaImagePaste(this.textarea, this.dropzone); + initTextareaPaste(this.textarea, this.dropzone); } } @@ -120,43 +132,41 @@ class ComboMarkdownEditor { setupTab() { const $container = $(this.container); - const $tabMenu = $container.find('.tabular.menu'); - const $tabs = $tabMenu.find('> .item'); + const tabs = $container[0].querySelectorAll('.tabular.menu > .item'); // Fomantic Tab requires the "data-tab" to be globally unique. // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic. - const $tabEditor = $tabs.filter(`.item[data-tab-for="markdown-writer"]`); - const $tabPreviewer = $tabs.filter(`.item[data-tab-for="markdown-previewer"]`); - $tabEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`); - $tabPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`); - const $panelEditor = $container.find('.ui.tab[data-tab-panel="markdown-writer"]'); - const $panelPreviewer = $container.find('.ui.tab[data-tab-panel="markdown-previewer"]'); - $panelEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`); - $panelPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`); + const tabEditor = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-writer'); + const tabPreviewer = Array.from(tabs).find((tab) => tab.getAttribute('data-tab-for') === 'markdown-previewer'); + tabEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`); + tabPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`); + const panelEditor = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-writer"]'); + const panelPreviewer = $container[0].querySelector('.ui.tab[data-tab-panel="markdown-previewer"]'); + panelEditor.setAttribute('data-tab', `markdown-writer-${elementIdCounter}`); + panelPreviewer.setAttribute('data-tab', `markdown-previewer-${elementIdCounter}`); elementIdCounter++; - $tabEditor[0].addEventListener('click', () => { + tabEditor.addEventListener('click', () => { requestAnimationFrame(() => { this.focus(); }); }); - $tabs.tab(); + $(tabs).tab(); - this.previewUrl = $tabPreviewer.attr('data-preview-url'); - this.previewContext = $tabPreviewer.attr('data-preview-context'); + this.previewUrl = tabPreviewer.getAttribute('data-preview-url'); + this.previewContext = tabPreviewer.getAttribute('data-preview-context'); this.previewMode = this.options.previewMode ?? 'comment'; this.previewWiki = this.options.previewWiki ?? false; - $tabPreviewer.on('click', () => { - $.post(this.previewUrl, { - _csrf: window.config.csrfToken, - mode: this.previewMode, - context: this.previewContext, - text: this.value(), - wiki: this.previewWiki, - }, (data) => { - renderPreviewPanelContent($panelPreviewer, data); - }); + tabPreviewer.addEventListener('click', async () => { + const formData = new FormData(); + formData.append('mode', this.previewMode); + formData.append('context', this.previewContext); + formData.append('text', this.value()); + formData.append('wiki', this.previewWiki); + const response = await POST(this.previewUrl, {data: formData}); + const data = await response.text(); + renderPreviewPanelContent($(panelPreviewer), data); }); } @@ -241,7 +251,7 @@ class ComboMarkdownEditor { }); this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights); await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true}); - initEasyMDEImagePaste(this.easyMDE, this.dropzone); + initEasyMDEPaste(this.easyMDE, this.dropzone); hideElem(this.textareaMarkdownToolbar); } diff --git a/web_src/js/features/comp/EasyMDEToolbarActions.js b/web_src/js/features/comp/EasyMDEToolbarActions.js index 8286d5d871..c97d683704 100644 --- a/web_src/js/features/comp/EasyMDEToolbarActions.js +++ b/web_src/js/features/comp/EasyMDEToolbarActions.js @@ -139,7 +139,7 @@ export function easyMDEToolbarActions(EasyMDE, editor) { }, icon: svg('octicon-chevron-right'), title: 'Add Inline Code', - } + }, }; for (const [key, value] of Object.entries(actions)) { diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js index 2a22190e10..2cc75cc6b0 100644 --- a/web_src/js/features/comp/LabelEdit.js +++ b/web_src/js/features/comp/LabelEdit.js @@ -1,40 +1,43 @@ import $ from 'jquery'; -import {initCompColorPicker} from './ColorPicker.js'; function isExclusiveScopeName(name) { return /.*[^/]\/[^/].*/.test(name); } function updateExclusiveLabelEdit(form) { - const nameInput = $(`${form} .label-name-input`); - const exclusiveField = $(`${form} .label-exclusive-input-field`); - const exclusiveCheckbox = $(`${form} .label-exclusive-input`); - const exclusiveWarning = $(`${form} .label-exclusive-warning`); + const nameInput = document.querySelector(`${form} .label-name-input`); + const exclusiveField = document.querySelector(`${form} .label-exclusive-input-field`); + const exclusiveCheckbox = document.querySelector(`${form} .label-exclusive-input`); + const exclusiveWarning = document.querySelector(`${form} .label-exclusive-warning`); - if (isExclusiveScopeName(nameInput.val())) { - exclusiveField.removeClass('muted'); - exclusiveField.removeAttr('aria-disabled'); - if (exclusiveCheckbox.prop('checked') && exclusiveCheckbox.data('exclusive-warn')) { - exclusiveWarning.removeClass('gt-hidden'); + if (isExclusiveScopeName(nameInput.value)) { + exclusiveField?.classList.remove('muted'); + exclusiveField?.removeAttribute('aria-disabled'); + if (exclusiveCheckbox.checked && exclusiveCheckbox.getAttribute('data-exclusive-warn')) { + exclusiveWarning?.classList.remove('tw-hidden'); } else { - exclusiveWarning.addClass('gt-hidden'); + exclusiveWarning?.classList.add('tw-hidden'); } } else { - exclusiveField.addClass('muted'); - exclusiveField.attr('aria-disabled', 'true'); - exclusiveWarning.addClass('gt-hidden'); + exclusiveField?.classList.add('muted'); + exclusiveField?.setAttribute('aria-disabled', 'true'); + exclusiveWarning?.classList.add('tw-hidden'); } } export function initCompLabelEdit(selector) { if (!$(selector).length) return; - initCompColorPicker(); // Create label $('.new-label.button').on('click', () => { updateExclusiveLabelEdit('.new-label'); $('.new-label.modal').modal({ onApprove() { + const form = document.querySelector('.new-label.form'); + if (!form.checkValidity()) { + form.reportValidity(); + return false; + } $('.new-label.form').trigger('submit'); }, }).modal('show'); @@ -43,29 +46,35 @@ export function initCompLabelEdit(selector) { // Edit label $('.edit-label-button').on('click', function () { - $('.edit-label .color-picker').minicolors('value', $(this).data('color')); $('#label-modal-id').val($(this).data('id')); - const nameInput = $('.edit-label .label-name-input'); - nameInput.val($(this).data('title')); + const $nameInput = $('.edit-label .label-name-input'); + $nameInput.val($(this).data('title')); - const isArchivedCheckbox = $('.edit-label .label-is-archived-input'); - isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived')); + const $isArchivedCheckbox = $('.edit-label .label-is-archived-input'); + $isArchivedCheckbox[0].checked = this.hasAttribute('data-is-archived'); - const exclusiveCheckbox = $('.edit-label .label-exclusive-input'); - exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive')); + const $exclusiveCheckbox = $('.edit-label .label-exclusive-input'); + $exclusiveCheckbox[0].checked = this.hasAttribute('data-exclusive'); // Warn when label was previously not exclusive and used in issues - exclusiveCheckbox.data('exclusive-warn', + $exclusiveCheckbox.data('exclusive-warn', $(this).data('num-issues') > 0 && - (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName(nameInput.val()))); + (!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val()))); updateExclusiveLabelEdit('.edit-label'); - $('.edit-label .label-desc-input').val($(this).data('description')); - $('.edit-label .color-picker').val($(this).data('color')); - $('.edit-label .minicolors-swatch-color').css('background-color', $(this).data('color')); + $('.edit-label .label-desc-input').val(this.getAttribute('data-description')); + + const colorInput = document.querySelector('.edit-label .js-color-picker-input input'); + colorInput.value = this.getAttribute('data-color'); + colorInput.dispatchEvent(new Event('input', {bubbles: true})); $('.edit-label.modal').modal({ onApprove() { + const form = document.querySelector('.edit-label.form'); + if (!form.checkValidity()) { + form.reportValidity(); + return false; + } $('.edit-label.form').trigger('submit'); }, }).modal('show'); diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/Paste.js similarity index 52% rename from web_src/js/features/comp/ImagePaste.js rename to web_src/js/features/comp/Paste.js index 27abcfe56f..b26296d1fc 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/Paste.js @@ -1,5 +1,8 @@ -import $ from 'jquery'; +import {htmlEscape} from 'escape-goat'; import {POST} from '../../modules/fetch.js'; +import {imageInfo} from '../../utils/image.js'; +import {getPastedContent, replaceTextareaSelection} from '../../utils/dom.js'; +import {isUrl} from '../../utils/url.js'; async function uploadFile(file, uploadUrl) { const formData = new FormData(); @@ -9,17 +12,6 @@ async function uploadFile(file, uploadUrl) { return await res.json(); } -function clipboardPastedImages(e) { - if (!e.clipboardData) return []; - - const files = []; - for (const item of e.clipboardData.items || []) { - if (!item.type || !item.type.startsWith('image/')) continue; - files.push(item.getAsFile()); - } - return files; -} - function triggerEditorContentChanged(target) { target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true})); } @@ -90,43 +82,73 @@ class CodeMirrorEditor { } } -const uploadClipboardImage = async (editor, dropzone, e) => { - const $dropzone = $(dropzone); - const uploadUrl = $dropzone.attr('data-upload-url'); - const $files = $dropzone.find('.files'); +async function handleClipboardImages(editor, dropzone, images, e) { + const uploadUrl = dropzone.getAttribute('data-upload-url'); + const filesContainer = dropzone.querySelector('.files'); - if (!uploadUrl || !$files.length) return; + if (!dropzone || !uploadUrl || !filesContainer || !images.length) return; - const pastedImages = clipboardPastedImages(e); - if (!pastedImages || pastedImages.length === 0) { - return; - } e.preventDefault(); e.stopPropagation(); - for (const img of pastedImages) { + for (const img of images) { const name = img.name.slice(0, img.name.lastIndexOf('.')); const placeholder = ``; editor.insertPlaceholder(placeholder); - const data = await uploadFile(img, uploadUrl); - editor.replacePlaceholder(placeholder, ``); - const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid); - $files.append($input); + const {uuid} = await uploadFile(img, uploadUrl); + const {width, dppx} = await imageInfo(img); + + const url = `/attachments/${uuid}`; + let text; + if (width > 0 && dppx > 1) { + // Scale down images from HiDPI monitors. This uses the <img> tag because it's the only + // method to change image size in Markdown that is supported by all implementations. + text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`; + } else { + text = ``; + } + editor.replacePlaceholder(placeholder, text); + + const input = document.createElement('input'); + input.setAttribute('name', 'files'); + input.setAttribute('type', 'hidden'); + input.setAttribute('id', uuid); + input.value = uuid; + filesContainer.append(input); } -}; +} -export function initEasyMDEImagePaste(easyMDE, dropzone) { - if (!dropzone) return; - easyMDE.codemirror.on('paste', async (_, e) => { - return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), dropzone, e); +function handleClipboardText(textarea, text, e) { + // when pasting links over selected text, turn it into [text](link), except when shift key is held + const {value, selectionStart, selectionEnd, _shiftDown} = textarea; + if (_shiftDown) return; + const selectedText = value.substring(selectionStart, selectionEnd); + const trimmedText = text.trim(); + if (selectedText && isUrl(trimmedText)) { + e.stopPropagation(); + e.preventDefault(); + replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`); + } +} + +export function initEasyMDEPaste(easyMDE, dropzone) { + easyMDE.codemirror.on('paste', (_, e) => { + const {images} = getPastedContent(e); + if (images.length) { + handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e); + } }); } -export function initTextareaImagePaste(textarea, dropzone) { - if (!dropzone) return; - $(textarea).on('paste', async (e) => { - return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e.originalEvent); +export function initTextareaPaste(textarea, dropzone) { + textarea.addEventListener('paste', (e) => { + const {images, text} = getPastedContent(e); + if (images.length) { + handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e); + } else if (text) { + handleClipboardText(textarea, text, e); + } }); } diff --git a/web_src/js/features/comp/QuickSubmit.js b/web_src/js/features/comp/QuickSubmit.js index 2587375a71..e6d7080bcf 100644 --- a/web_src/js/features/comp/QuickSubmit.js +++ b/web_src/js/features/comp/QuickSubmit.js @@ -1,5 +1,3 @@ -import $ from 'jquery'; - export function handleGlobalEnterQuickSubmit(target) { const form = target.closest('form'); if (form) { @@ -8,14 +6,9 @@ export function handleGlobalEnterQuickSubmit(target) { return; } - if (form.classList.contains('form-fetch-action')) { - form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true})); - return; - } - // here use the event to trigger the submit event (instead of calling `submit()` method directly) // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog - $(form).trigger('submit'); + form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true})); } else { // if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request. // the 'ce-' prefix means this is a CustomEvent diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js index 76834f8844..2def3db51a 100644 --- a/web_src/js/features/comp/ReactionSelector.js +++ b/web_src/js/features/comp/ReactionSelector.js @@ -5,11 +5,11 @@ export function initCompReactionSelector($parent) { $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) { e.preventDefault(); - if ($(this).hasClass('disabled')) return; + if (this.classList.contains('disabled')) return; - const actionUrl = $(this).closest('[data-action-url]').attr('data-action-url'); - const reactionContent = $(this).attr('data-reaction-content'); - const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true'; + const actionUrl = this.closest('[data-action-url]')?.getAttribute('data-action-url'); + const reactionContent = this.getAttribute('data-reaction-content'); + const hasReacted = this.closest('.ui.segment.reactions')?.querySelector(`a[data-reaction-content="${reactionContent}"]`)?.getAttribute('data-has-reacted') === 'true'; const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, { data: new URLSearchParams({content: reactionContent}), @@ -17,21 +17,21 @@ export function initCompReactionSelector($parent) { const data = await res.json(); if (data && (data.html || data.empty)) { - const content = $(this).closest('.content'); - let react = content.find('.segment.reactions'); - if ((!data.empty || data.html === '') && react.length > 0) { - react.remove(); + const $content = $(this).closest('.content'); + let $react = $content.find('.segment.reactions'); + if ((!data.empty || data.html === '') && $react.length > 0) { + $react.remove(); } if (!data.empty) { - const attachments = content.find('.segment.bottom:first'); - react = $(data.html); - if (attachments.length > 0) { - react.insertBefore(attachments); + const $attachments = $content.find('.segment.bottom:first'); + $react = $(data.html); + if ($attachments.length > 0) { + $react.insertBefore($attachments); } else { - react.appendTo(content); + $react.appendTo($content); } - react.find('.dropdown').dropdown(); - initCompReactionSelector(react); + $react.find('.dropdown').dropdown(); + initCompReactionSelector($react); } } }); diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js index 992d4ef020..081c47425f 100644 --- a/web_src/js/features/comp/SearchUserBox.js +++ b/web_src/js/features/comp/SearchUserBox.js @@ -5,9 +5,12 @@ const {appSubUrl} = window.config; const looksLikeEmailAddressCheck = /^\S+@\S+$/; export function initCompSearchUserBox() { - const $searchUserBox = $('#search-user-box'); - const allowEmailInput = $searchUserBox.attr('data-allow-email') === 'true'; - const allowEmailDescription = $searchUserBox.attr('data-allow-email-description'); + const searchUserBox = document.getElementById('search-user-box'); + if (!searchUserBox) return; + + const $searchUserBox = $(searchUserBox); + const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true'; + const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined; $searchUserBox.search({ minCharacters: 2, apiSettings: { @@ -19,7 +22,7 @@ export function initCompSearchUserBox() { $.each(response.data, (_i, item) => { const resultItem = { title: item.login, - image: item.avatar_url + image: item.avatar_url, }; if (item.full_name) { resultItem.description = htmlEscape(item.full_name); @@ -31,18 +34,18 @@ export function initCompSearchUserBox() { } }); - if (allowEmailInput && items.length === 0 && looksLikeEmailAddressCheck.test(searchQuery)) { + if (allowEmailInput && !items.length && looksLikeEmailAddressCheck.test(searchQuery)) { const resultItem = { title: searchQuery, - description: allowEmailDescription + description: allowEmailDescription, }; items.push(resultItem); } return {results: items}; - } + }, }, searchFields: ['login', 'full_name'], - showNoResults: false + showNoResults: false, }); } diff --git a/web_src/js/features/comp/WebHookEditor.js b/web_src/js/features/comp/WebHookEditor.js index f4c82898fd..d74b59fd2a 100644 --- a/web_src/js/features/comp/WebHookEditor.js +++ b/web_src/js/features/comp/WebHookEditor.js @@ -1,43 +1,44 @@ -import $ from 'jquery'; +import {POST} from '../../modules/fetch.js'; import {hideElem, showElem, toggleElem} from '../../utils/dom.js'; -const {csrfToken} = window.config; - export function initCompWebHookEditor() { - if ($('.new.webhook').length === 0) { + if (!document.querySelectorAll('.new.webhook').length) { return; } - $('.events.checkbox input').on('change', function () { - if ($(this).is(':checked')) { - showElem($('.events.fields')); - } - }); - $('.non-events.checkbox input').on('change', function () { - if ($(this).is(':checked')) { - hideElem($('.events.fields')); - } - }); + for (const input of document.querySelectorAll('.events.checkbox input')) { + input.addEventListener('change', function () { + if (this.checked) { + showElem('.events.fields'); + } + }); + } - const updateContentType = function () { - const visible = $('#http_method').val() === 'POST'; - toggleElem($('#content_type').parent().parent(), visible); - }; - updateContentType(); - $('#http_method').on('change', () => { + for (const input of document.querySelectorAll('.non-events.checkbox input')) { + input.addEventListener('change', function () { + if (this.checked) { + hideElem('.events.fields'); + } + }); + } + + // some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field + const httpMethodInput = document.getElementById('http_method'); + if (httpMethodInput) { + const updateContentType = function () { + const visible = httpMethodInput.value === 'POST'; + toggleElem(document.getElementById('content_type').closest('.field'), visible); + }; updateContentType(); - }); + httpMethodInput.addEventListener('change', updateContentType); + } // Test delivery - $('#test-delivery').on('click', function () { - const $this = $(this); - $this.addClass('loading disabled'); - $.post($this.data('link'), { - _csrf: csrfToken - }).done( - setTimeout(() => { - window.location.href = $this.data('redirect'); - }, 5000) - ); + document.getElementById('test-delivery')?.addEventListener('click', async function () { + this.classList.add('is-loading', 'disabled'); + await POST(this.getAttribute('data-link')); + setTimeout(() => { + window.location.href = this.getAttribute('data-redirect'); + }, 5000); }); } diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js index 23a620b8a2..ce90f3e505 100644 --- a/web_src/js/features/contextpopup.js +++ b/web_src/js/features/contextpopup.js @@ -1,11 +1,10 @@ -import $ from 'jquery'; import {createApp} from 'vue'; import ContextPopup from '../components/ContextPopup.vue'; import {parseIssueHref} from '../utils.js'; import {createTippy} from '../modules/tippy.js'; export function initContextPopups() { - const refIssues = $('.ref-issue'); + const refIssues = document.querySelectorAll('.ref-issue'); attachRefIssueContextPopup(refIssues); } @@ -38,7 +37,7 @@ export function attachRefIssueContextPopup(refIssues) { interactiveBorder: 5, onShow: () => { el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}})); - } + }, }); } } diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js new file mode 100644 index 0000000000..1d9cba5b9b --- /dev/null +++ b/web_src/js/features/contributors.js @@ -0,0 +1,28 @@ +import {createApp} from 'vue'; + +export async function initRepoContributors() { + const el = document.getElementById('repo-contributors-chart'); + if (!el) return; + + const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue'); + try { + const View = createApp(RepoContributors, { + locale: { + filterLabel: el.getAttribute('data-locale-filter-label'), + contributionType: { + commits: el.getAttribute('data-locale-contribution-type-commits'), + additions: el.getAttribute('data-locale-contribution-type-additions'), + deletions: el.getAttribute('data-locale-contribution-type-deletions'), + }, + + loadingTitle: el.getAttribute('data-locale-loading-title'), + loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'), + loadingInfo: el.getAttribute('data-locale-loading-info'), + }, + }); + View.mount(el); + } catch (err) { + console.error('RepoContributors failed to load', err); + el.textContent = el.getAttribute('data-locale-component-failed-to-load'); + } +} diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js index 3d3b2a697e..03efe00701 100644 --- a/web_src/js/features/copycontent.js +++ b/web_src/js/features/copycontent.js @@ -19,7 +19,7 @@ export function initCopyContent() { // the text to copy is not in the DOM or it is an image which should be // fetched to copy in full resolution if (link) { - btn.classList.add('is-loading', 'small-loading-icon'); + btn.classList.add('is-loading', 'loading-icon-2px'); try { const res = await GET(link, {credentials: 'include', redirect: 'follow'}); const contentType = res.headers.get('content-type'); @@ -33,7 +33,7 @@ export function initCopyContent() { } catch { return showTemporaryTooltip(btn, i18n.copy_error); } finally { - btn.classList.remove('is-loading', 'small-loading-icon'); + btn.classList.remove('is-loading', 'loading-icon-2px'); } } else { // text, read from DOM const lineEls = document.querySelectorAll('.file-view .lines-code'); diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js index 2ac7d93cc1..62581cf687 100644 --- a/web_src/js/features/eventsource.sharedworker.js +++ b/web_src/js/features/eventsource.sharedworker.js @@ -48,7 +48,7 @@ class Source { this.eventSource.addEventListener(eventType, (event) => { this.notifyClients({ type: eventType, - data: event.data + data: event.data, }); }); } diff --git a/web_src/js/features/imagediff.js b/web_src/js/features/imagediff.js index 80b7e83385..192a642834 100644 --- a/web_src/js/features/imagediff.js +++ b/web_src/js/features/imagediff.js @@ -20,19 +20,19 @@ function getDefaultSvgBoundsIfUndefined(text, src) { if (img.width > 1 && img.width < MaxSize && img.height > 1 && img.height < MaxSize) { return { width: img.width, - height: img.height + height: img.height, }; } if (svg.hasAttribute('viewBox')) { const viewBox = svg.viewBox.baseVal; return { width: DefaultSize, - height: DefaultSize * viewBox.width / viewBox.height + height: DefaultSize * viewBox.width / viewBox.height, }; } return { width: DefaultSize, - height: DefaultSize + height: DefaultSize, }; } return null; @@ -42,20 +42,20 @@ export function initImageDiff() { function createContext(image1, image2) { const size1 = { width: image1 && image1.width || 0, - height: image1 && image1.height || 0 + height: image1 && image1.height || 0, }; const size2 = { width: image2 && image2.width || 0, - height: image2 && image2.height || 0 + height: image2 && image2.height || 0, }; const max = { width: Math.max(size2.width, size1.width), - height: Math.max(size2.height, size1.height) + height: Math.max(size2.height, size1.height), }; return { - image1: $(image1), - image2: $(image2), + $image1: $(image1), + $image2: $(image2), size1, size2, max, @@ -63,14 +63,14 @@ export function initImageDiff() { Math.floor(max.width - size1.width) / 2, Math.floor(max.height - size1.height) / 2, Math.floor(max.width - size2.width) / 2, - Math.floor(max.height - size2.height) / 2 - ] + Math.floor(max.height - size2.height) / 2, + ], }; } $('.image-diff:not([data-image-diff-loaded])').each(async function() { const $container = $(this); - $container.attr('data-image-diff-loaded', 'true'); + this.setAttribute('data-image-diff-loaded', 'true'); // the container may be hidden by "viewed" checkbox, so use the parent's width for reference const diffContainerWidth = Math.max($container.closest('.diff-file-box').width() - 300, 100); @@ -79,12 +79,12 @@ export function initImageDiff() { path: this.getAttribute('data-path-after'), mime: this.getAttribute('data-mime-after'), $images: $container.find('img.image-after'), // matches 3 <img> - $boundsInfo: $container.find('.bounds-info-after') + $boundsInfo: $container.find('.bounds-info-after'), }, { path: this.getAttribute('data-path-before'), mime: this.getAttribute('data-mime-before'), $images: $container.find('img.image-before'), // matches 3 <img> - $boundsInfo: $container.find('.bounds-info-before') + $boundsInfo: $container.find('.bounds-info-before'), }]; await Promise.all(imageInfos.map(async (info) => { @@ -98,8 +98,10 @@ export function initImageDiff() { const text = await resp.text(); const bounds = getDefaultSvgBoundsIfUndefined(text, info.path); if (bounds) { - info.$images.attr('width', bounds.width); - info.$images.attr('height', bounds.height); + info.$images.each(function() { + this.setAttribute('width', bounds.width); + this.setAttribute('height', bounds.height); + }); hideElem(info.$boundsInfo); } } @@ -108,49 +110,61 @@ export function initImageDiff() { const $imagesAfter = imageInfos[0].$images; const $imagesBefore = imageInfos[1].$images; - initSideBySide(createContext($imagesAfter[0], $imagesBefore[0])); + initSideBySide(this, createContext($imagesAfter[0], $imagesBefore[0])); if ($imagesAfter.length > 0 && $imagesBefore.length > 0) { initSwipe(createContext($imagesAfter[1], $imagesBefore[1])); initOverlay(createContext($imagesAfter[2], $imagesBefore[2])); } - $container.find('> .image-diff-tabs').removeClass('is-loading'); + this.querySelector(':scope > .image-diff-tabs')?.classList.remove('is-loading'); - function initSideBySide(sizes) { + function initSideBySide(container, sizes) { let factor = 1; if (sizes.max.width > (diffContainerWidth - 24) / 2) { factor = (diffContainerWidth - 24) / 2 / sizes.max.width; } - const widthChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalWidth !== sizes.image2[0].naturalWidth; - const heightChanged = sizes.image1.length !== 0 && sizes.image2.length !== 0 && sizes.image1[0].naturalHeight !== sizes.image2[0].naturalHeight; - if (sizes.image1.length !== 0) { - $container.find('.bounds-info-after .bounds-info-width').text(`${sizes.image1[0].naturalWidth}px`).addClass(widthChanged ? 'green' : ''); - $container.find('.bounds-info-after .bounds-info-height').text(`${sizes.image1[0].naturalHeight}px`).addClass(heightChanged ? 'green' : ''); - } - if (sizes.image2.length !== 0) { - $container.find('.bounds-info-before .bounds-info-width').text(`${sizes.image2[0].naturalWidth}px`).addClass(widthChanged ? 'red' : ''); - $container.find('.bounds-info-before .bounds-info-height').text(`${sizes.image2[0].naturalHeight}px`).addClass(heightChanged ? 'red' : ''); + const widthChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalWidth !== sizes.$image2[0].naturalWidth; + const heightChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalHeight !== sizes.$image2[0].naturalHeight; + if (sizes.$image1?.length) { + const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width'); + boundsInfoAfterWidth.textContent = `${sizes.$image1[0].naturalWidth}px`; + if (widthChanged) boundsInfoAfterWidth.classList.add('green'); + + const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height'); + boundsInfoAfterHeight.textContent = `${sizes.$image1[0].naturalHeight}px`; + if (heightChanged) boundsInfoAfterHeight.classList.add('green'); } - sizes.image1.css({ - width: sizes.size1.width * factor, - height: sizes.size1.height * factor - }); - sizes.image1.parent().css({ - margin: `10px auto`, - width: sizes.size1.width * factor + 2, - height: sizes.size1.height * factor + 2 - }); - sizes.image2.css({ - width: sizes.size2.width * factor, - height: sizes.size2.height * factor - }); - sizes.image2.parent().css({ - margin: `10px auto`, - width: sizes.size2.width * factor + 2, - height: sizes.size2.height * factor + 2 - }); + if (sizes.$image2?.length) { + const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width'); + boundsInfoBeforeWidth.textContent = `${sizes.$image2[0].naturalWidth}px`; + if (widthChanged) boundsInfoBeforeWidth.classList.add('red'); + + const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height'); + boundsInfoBeforeHeight.textContent = `${sizes.$image2[0].naturalHeight}px`; + if (heightChanged) boundsInfoBeforeHeight.classList.add('red'); + } + + const image1 = sizes.$image1[0]; + if (image1) { + const container = image1.parentNode; + image1.style.width = `${sizes.size1.width * factor}px`; + image1.style.height = `${sizes.size1.height * factor}px`; + container.style.margin = '10px auto'; + container.style.width = `${sizes.size1.width * factor + 2}px`; + container.style.height = `${sizes.size1.height * factor + 2}px`; + } + + const image2 = sizes.$image2[0]; + if (image2) { + const container = image2.parentNode; + image2.style.width = `${sizes.size2.width * factor}px`; + image2.style.height = `${sizes.size2.height * factor}px`; + container.style.margin = '10px auto'; + container.style.width = `${sizes.size2.width * factor + 2}px`; + container.style.height = `${sizes.size2.height * factor + 2}px`; + } } function initSwipe(sizes) { @@ -159,36 +173,39 @@ export function initImageDiff() { factor = (diffContainerWidth - 12) / sizes.max.width; } - sizes.image1.css({ - width: sizes.size1.width * factor, - height: sizes.size1.height * factor - }); - sizes.image1.parent().css({ - margin: `0px ${sizes.ratio[0] * factor}px`, - width: sizes.size1.width * factor + 2, - height: sizes.size1.height * factor + 2 - }); - sizes.image1.parent().parent().css({ - padding: `${sizes.ratio[1] * factor}px 0 0 0`, - width: sizes.max.width * factor + 2 - }); - sizes.image2.css({ - width: sizes.size2.width * factor, - height: sizes.size2.height * factor - }); - sizes.image2.parent().css({ - margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`, - width: sizes.size2.width * factor + 2, - height: sizes.size2.height * factor + 2 - }); - sizes.image2.parent().parent().css({ - width: sizes.max.width * factor + 2, - height: sizes.max.height * factor + 2 - }); - $container.find('.diff-swipe').css({ - width: sizes.max.width * factor + 2, - height: sizes.max.height * factor + 30 /* extra height for inner "position: absolute" elements */, - }); + const image1 = sizes.$image1[0]; + if (image1) { + const container = image1.parentNode; + const swipeFrame = container.parentNode; + image1.style.width = `${sizes.size1.width * factor}px`; + image1.style.height = `${sizes.size1.height * factor}px`; + container.style.margin = `0px ${sizes.ratio[0] * factor}px`; + container.style.width = `${sizes.size1.width * factor + 2}px`; + container.style.height = `${sizes.size1.height * factor + 2}px`; + swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`; + swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; + } + + const image2 = sizes.$image2[0]; + if (image2) { + const container = image2.parentNode; + const swipeFrame = container.parentNode; + image2.style.width = `${sizes.size2.width * factor}px`; + image2.style.height = `${sizes.size2.height * factor}px`; + container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; + container.style.width = `${sizes.size2.width * factor + 2}px`; + container.style.height = `${sizes.size2.height * factor + 2}px`; + swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; + swipeFrame.style.height = `${sizes.max.height * factor + 2}px`; + } + + // extra height for inner "position: absolute" elements + const swipe = $container.find('.diff-swipe')[0]; + if (swipe) { + swipe.style.width = `${sizes.max.width * factor + 2}px`; + swipe.style.height = `${sizes.max.height * factor + 30}px`; + } + $container.find('.swipe-bar').on('mousedown', function(e) { e.preventDefault(); @@ -200,13 +217,9 @@ export function initImageDiff() { e2.preventDefault(); const value = Math.max(0, Math.min(e2.clientX - $swipeFrame.offset().left, width)); + $swipeBar[0].style.left = `${value}px`; + $container.find('.swipe-container')[0].style.width = `${$swipeFrame.width() - value}px`; - $swipeBar.css({ - left: value - }); - $container.find('.swipe-container').css({ - width: $swipeFrame.width() - value - }); $(document).on('mouseup.diff-swipe', () => { $(document).off('.diff-swipe'); }); @@ -220,38 +233,39 @@ export function initImageDiff() { factor = (diffContainerWidth - 12) / sizes.max.width; } - sizes.image1.css({ - width: sizes.size1.width * factor, - height: sizes.size1.height * factor - }); - sizes.image2.css({ - width: sizes.size2.width * factor, - height: sizes.size2.height * factor - }); - sizes.image1.parent().css({ - margin: `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`, - width: sizes.size1.width * factor + 2, - height: sizes.size1.height * factor + 2 - }); - sizes.image2.parent().css({ - margin: `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`, - width: sizes.size2.width * factor + 2, - height: sizes.size2.height * factor + 2 - }); + const image1 = sizes.$image1[0]; + if (image1) { + const container = image1.parentNode; + image1.style.width = `${sizes.size1.width * factor}px`; + image1.style.height = `${sizes.size1.height * factor}px`; + container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`; + container.style.width = `${sizes.size1.width * factor + 2}px`; + container.style.height = `${sizes.size1.height * factor + 2}px`; + } - // some inner elements are `position: absolute`, so the container's height must be large enough - // the "css(width, height)" is somewhat hacky and not easy to understand, it could be improved in the future - sizes.image2.parent().parent().css({ - width: sizes.max.width * factor + 2, - height: sizes.max.height * factor + 2, - }); + const image2 = sizes.$image2[0]; + if (image2) { + const container = image2.parentNode; + const overlayFrame = container.parentNode; + image2.style.width = `${sizes.size2.width * factor}px`; + image2.style.height = `${sizes.size2.height * factor}px`; + container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; + container.style.width = `${sizes.size2.width * factor + 2}px`; + container.style.height = `${sizes.size2.height * factor + 2}px`; - const $range = $container.find("input[type='range']"); - const onInput = () => sizes.image1.parent().css({ - opacity: $range.val() / 100 - }); - $range.on('input', onInput); - onInput(); + // some inner elements are `position: absolute`, so the container's height must be large enough + overlayFrame.style.width = `${sizes.max.width * factor + 2}px`; + overlayFrame.style.height = `${sizes.max.height * factor + 2}px`; + } + + const rangeInput = $container[0].querySelector('input[type="range"]'); + function updateOpacity() { + if (sizes?.$image1?.[0]) { + sizes.$image1[0].parentNode.style.opacity = `${rangeInput.value / 100}`; + } + } + rangeInput?.addEventListener('input', updateOpacity); + updateOpacity(); } }); } diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js index 9fda7f7d27..54ba3778f8 100644 --- a/web_src/js/features/install.js +++ b/web_src/js/features/install.js @@ -1,19 +1,17 @@ -import $ from 'jquery'; import {hideElem, showElem} from '../utils/dom.js'; import {GET} from '../modules/fetch.js'; export function initInstall() { - const $page = $('.page-content.install'); - if ($page.length === 0) { + const page = document.querySelector('.page-content.install'); + if (!page) { return; } - if ($page.is('.post-install')) { + if (page.classList.contains('post-install')) { initPostInstall(); } else { initPreInstall(); } } - function initPreInstall() { const defaultDbUser = 'gitea'; const defaultDbName = 'gitea'; @@ -21,86 +19,85 @@ function initPreInstall() { const defaultDbHosts = { mysql: '127.0.0.1:3306', postgres: '127.0.0.1:5432', - mssql: '127.0.0.1:1433' + mssql: '127.0.0.1:1433', }; - const $dbHost = $('#db_host'); - const $dbUser = $('#db_user'); - const $dbName = $('#db_name'); + const dbHost = document.getElementById('db_host'); + const dbUser = document.getElementById('db_user'); + const dbName = document.getElementById('db_name'); // Database type change detection. - $('#db_type').on('change', function () { - const dbType = $(this).val(); - hideElem($('div[data-db-setting-for]')); - showElem($(`div[data-db-setting-for=${dbType}]`)); + document.getElementById('db_type').addEventListener('change', function () { + const dbType = this.value; + hideElem('div[data-db-setting-for]'); + showElem(`div[data-db-setting-for=${dbType}]`); if (dbType !== 'sqlite3') { // for most remote database servers - showElem($(`div[data-db-setting-for=common-host]`)); - const lastDbHost = $dbHost.val(); + showElem('div[data-db-setting-for=common-host]'); + const lastDbHost = dbHost.value; const isDbHostDefault = !lastDbHost || Object.values(defaultDbHosts).includes(lastDbHost); if (isDbHostDefault) { - $dbHost.val(defaultDbHosts[dbType] ?? ''); + dbHost.value = defaultDbHosts[dbType] ?? ''; } - if (!$dbUser.val() && !$dbName.val()) { - $dbUser.val(defaultDbUser); - $dbName.val(defaultDbName); + if (!dbUser.value && !dbName.value) { + dbUser.value = defaultDbUser; + dbName.value = defaultDbName; } } // else: for SQLite3, the default path is always prepared by backend code (setting) - }).trigger('change'); + }); + document.getElementById('db_type').dispatchEvent(new Event('change')); - const $appUrl = $('#app_url'); - const configAppUrl = $appUrl.val(); - if (configAppUrl.includes('://localhost')) { - $appUrl.val(window.location.href); + const appUrl = document.getElementById('app_url'); + if (appUrl.value.includes('://localhost')) { + appUrl.value = window.location.href; } - const $domain = $('#domain'); - const configDomain = $domain.val().trim(); - if (configDomain === 'localhost') { - $domain.val(window.location.hostname); + const domain = document.getElementById('domain'); + if (domain.value.trim() === 'localhost') { + domain.value = window.location.hostname; } // TODO: better handling of exclusive relations. - $('#offline-mode input').on('change', function () { - if ($(this).is(':checked')) { - $('#disable-gravatar').checkbox('check'); - $('#federated-avatar-lookup').checkbox('uncheck'); + document.querySelector('#offline-mode input').addEventListener('change', function () { + if (this.checked) { + document.querySelector('#disable-gravatar input').checked = true; + document.querySelector('#federated-avatar-lookup input').checked = false; } }); - $('#disable-gravatar input').on('change', function () { - if ($(this).is(':checked')) { - $('#federated-avatar-lookup').checkbox('uncheck'); + document.querySelector('#disable-gravatar input').addEventListener('change', function () { + if (this.checked) { + document.querySelector('#federated-avatar-lookup input').checked = false; } else { - $('#offline-mode').checkbox('uncheck'); + document.querySelector('#offline-mode input').checked = false; } }); - $('#federated-avatar-lookup input').on('change', function () { - if ($(this).is(':checked')) { - $('#disable-gravatar').checkbox('uncheck'); - $('#offline-mode').checkbox('uncheck'); + document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () { + if (this.checked) { + document.querySelector('#disable-gravatar input').checked = false; + document.querySelector('#offline-mode input').checked = false; } }); - $('#enable-openid-signin input').on('change', function () { - if ($(this).is(':checked')) { - if (!$('#disable-registration input').is(':checked')) { - $('#enable-openid-signup').checkbox('check'); + document.querySelector('#enable-openid-signin input').addEventListener('change', function () { + if (this.checked) { + if (!document.querySelector('#disable-registration input').checked) { + document.querySelector('#enable-openid-signup input').checked = true; } } else { - $('#enable-openid-signup').checkbox('uncheck'); + document.querySelector('#enable-openid-signup input').checked = false; } }); - $('#disable-registration input').on('change', function () { - if ($(this).is(':checked')) { - $('#enable-captcha').checkbox('uncheck'); - $('#enable-openid-signup').checkbox('uncheck'); + document.querySelector('#disable-registration input').addEventListener('change', function () { + if (this.checked) { + document.querySelector('#enable-captcha input').checked = false; + document.querySelector('#enable-openid-signup input').checked = false; } else { - $('#enable-openid-signup').checkbox('check'); + document.querySelector('#enable-openid-signup input').checked = true; } }); - $('#enable-captcha input').on('change', function () { - if ($(this).is(':checked')) { - $('#disable-registration').checkbox('uncheck'); + document.querySelector('#enable-captcha input').addEventListener('change', function () { + if (this.checked) { + document.querySelector('#disable-registration input').checked = false; } }); } diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js index 4dcf02d2dc..2de640e674 100644 --- a/web_src/js/features/notification.js +++ b/web_src/js/features/notification.js @@ -1,6 +1,8 @@ import $ from 'jquery'; +import {GET} from '../modules/fetch.js'; +import {toggleElem} from '../utils/dom.js'; -const {appSubUrl, csrfToken, notificationSettings, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; let notificationSequenceNumber = 0; export function initNotificationsTable() { @@ -27,25 +29,6 @@ export function initNotificationsTable() { e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); }); } - - $('#notification_table .button').on('click', function () { - (async () => { - const data = await updateNotification( - $(this).data('url'), - $(this).data('status'), - $(this).data('page'), - $(this).data('q'), - $(this).data('notification-id'), - ); - - if ($(data).data('sequence-number') === notificationSequenceNumber) { - $('#notification_div').replaceWith(data); - initNotificationsTable(); - } - await updateNotificationCount(); - })(); - return false; - }); } async function receiveUpdateCount(event) { @@ -53,7 +36,7 @@ async function receiveUpdateCount(event) { const data = JSON.parse(event.data); for (const count of document.querySelectorAll('.notification_count')) { - count.classList.toggle('gt-hidden', data.Count === 0); + count.classList.toggle('tw-hidden', data.Count === 0); count.textContent = `${data.Count}`; } await updateNotificationTable(); @@ -63,9 +46,9 @@ async function receiveUpdateCount(event) { } export function initNotificationCount() { - const notificationCount = $('.notification_count'); + const $notificationCount = $('.notification_count'); - if (!notificationCount.length) { + if (!$notificationCount.length) { return; } @@ -73,7 +56,7 @@ export function initNotificationCount() { const startPeriodicPoller = (timeout, lastCount) => { if (timeout <= 0 || !Number.isFinite(timeout)) return; usingPeriodicPoller = true; - lastCount = lastCount ?? notificationCount.text(); + lastCount = lastCount ?? $notificationCount.text(); setTimeout(async () => { await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount); }, timeout); @@ -112,7 +95,7 @@ export function initNotificationCount() { type: 'close', }); worker.port.close(); - window.location.href = appSubUrl; + window.location.href = `${appSubUrl}/`; } else if (event.data.type === 'close') { worker.port.postMessage({ type: 'close', @@ -161,60 +144,49 @@ async function updateNotificationCountWithCallback(callback, timeout, lastCount) } async function updateNotificationTable() { - const notificationDiv = $('#notification_div'); - if (notificationDiv.length > 0) { - const data = await $.ajax({ - type: 'GET', - url: `${appSubUrl}/notifications${window.location.search}`, - data: { - 'div-only': true, - 'sequence-number': ++notificationSequenceNumber, + const notificationDiv = document.getElementById('notification_div'); + if (notificationDiv) { + try { + const params = new URLSearchParams(window.location.search); + params.set('div-only', true); + params.set('sequence-number', ++notificationSequenceNumber); + const url = `${appSubUrl}/notifications?${params.toString()}`; + const response = await GET(url); + + if (!response.ok) { + throw new Error('Failed to fetch notification table'); } - }); - if ($(data).data('sequence-number') === notificationSequenceNumber) { - notificationDiv.replaceWith(data); - initNotificationsTable(); + + const data = await response.text(); + if ($(data).data('sequence-number') === notificationSequenceNumber) { + notificationDiv.outerHTML = data; + initNotificationsTable(); + } + } catch (error) { + console.error(error); } } } async function updateNotificationCount() { - const data = await $.ajax({ - type: 'GET', - url: `${appSubUrl}/notifications/new`, - headers: { - 'X-Csrf-Token': csrfToken, - }, - }); + try { + const response = await GET(`${appSubUrl}/notifications/new`); - const notificationCount = $('.notification_count'); - if (data.new === 0) { - notificationCount.addClass('gt-hidden'); - } else { - notificationCount.removeClass('gt-hidden'); + if (!response.ok) { + throw new Error('Failed to fetch notification count'); + } + + const data = await response.json(); + + toggleElem('.notification_count', data.new !== 0); + + for (const el of document.getElementsByClassName('notification_count')) { + el.textContent = `${data.new}`; + } + + return `${data.new}`; + } catch (error) { + console.error(error); + return '0'; } - - notificationCount.text(`${data.new}`); - - return `${data.new}`; -} - -async function updateNotification(url, status, page, q, notificationID) { - if (status !== 'pinned') { - $(`#notification_${notificationID}`).remove(); - } - - return $.ajax({ - type: 'POST', - url, - data: { - _csrf: csrfToken, - notification_id: notificationID, - status, - page, - q, - noredirect: true, - 'sequence-number': ++notificationSequenceNumber, - }, - }); } diff --git a/web_src/js/features/org-team.js b/web_src/js/features/org-team.js index 6ae3a90f4d..c216fdf6a2 100644 --- a/web_src/js/features/org-team.js +++ b/web_src/js/features/org-team.js @@ -8,9 +8,9 @@ export function initOrgTeamSettings() { $('.organization.new.team input[name=permission]').on('change', () => { const val = $('input[name=permission]:checked', '.organization.new.team').val(); if (val === 'admin') { - hideElem($('.organization.new.team .team-units')); + hideElem('.organization.new.team .team-units'); } else { - showElem($('.organization.new.team .team-units')); + showElem('.organization.new.team .team-units'); } }); } @@ -26,14 +26,14 @@ export function initOrgTeamSearchRepoBox() { $.each(response.data, (_i, item) => { items.push({ title: item.repository.full_name.split('/')[1], - description: item.repository.full_name + description: item.repository.full_name, }); }); return {results: items}; - } + }, }, searchFields: ['full_name'], - showNoResults: false + showNoResults: false, }); } diff --git a/web_src/js/features/pull-view-file.js b/web_src/js/features/pull-view-file.js index 90881ee989..2472e5a0bd 100644 --- a/web_src/js/features/pull-view-file.js +++ b/web_src/js/features/pull-view-file.js @@ -43,9 +43,11 @@ export function initViewedCheckboxListenerFor() { // Mark the file as viewed visually - will especially change the background if (this.checked) { form.classList.add(viewedStyleClass); + checkbox.setAttribute('checked', ''); prReview.numberOfViewedFiles++; } else { form.classList.remove(viewedStyleClass); + checkbox.removeAttribute('checked'); prReview.numberOfViewedFiles--; } diff --git a/web_src/js/features/recent-commits.js b/web_src/js/features/recent-commits.js new file mode 100644 index 0000000000..030c251a05 --- /dev/null +++ b/web_src/js/features/recent-commits.js @@ -0,0 +1,21 @@ +import {createApp} from 'vue'; + +export async function initRepoRecentCommits() { + const el = document.getElementById('repo-recent-commits-chart'); + if (!el) return; + + const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue'); + try { + const View = createApp(RepoRecentCommits, { + locale: { + loadingTitle: el.getAttribute('data-locale-loading-title'), + loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'), + loadingInfo: el.getAttribute('data-locale-loading-info'), + }, + }); + View.mount(el); + } catch (err) { + console.error('RepoRecentCommits failed to load', err); + el.textContent = el.getAttribute('data-locale-component-failed-to-load'); + } +} diff --git a/web_src/js/features/repo-branch.js b/web_src/js/features/repo-branch.js index e6da9661b6..b9ffc6127f 100644 --- a/web_src/js/features/repo-branch.js +++ b/web_src/js/features/repo-branch.js @@ -8,35 +8,35 @@ export function initRepoBranchButton() { function initRepoCreateBranchButton() { // 2 pages share this code, one is the branch list page, the other is the commit view page: create branch/tag from current commit (dirty code) - $('.show-create-branch-modal').on('click', function () { - let modalFormName = $(this).attr('data-modal-form'); - if (!modalFormName) { - modalFormName = '#create-branch-form'; - } - $(modalFormName)[0].action = $(modalFormName).attr('data-base-action') + $(this).attr('data-branch-from-urlcomponent'); - let fromSpanName = $(this).attr('data-modal-from-span'); - if (!fromSpanName) { - fromSpanName = '#modal-create-branch-from-span'; - } + for (const el of document.querySelectorAll('.show-create-branch-modal')) { + el.addEventListener('click', () => { + const modalFormName = el.getAttribute('data-modal-form') || '#create-branch-form'; + const modalForm = document.querySelector(modalFormName); + if (!modalForm) return; + modalForm.action = `${modalForm.getAttribute('data-base-action')}${el.getAttribute('data-branch-from-urlcomponent')}`; - $(fromSpanName).text($(this).attr('data-branch-from')); - $($(this).attr('data-modal')).modal('show'); - }); + const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span'; + document.querySelector(fromSpanName).textContent = el.getAttribute('data-branch-from'); + + $(el.getAttribute('data-modal')).modal('show'); + }); + } } function initRepoRenameBranchButton() { - $('.show-rename-branch-modal').on('click', function () { - const target = $(this).attr('data-modal'); - const $modal = $(target); + for (const el of document.querySelectorAll('.show-rename-branch-modal')) { + el.addEventListener('click', () => { + const target = el.getAttribute('data-modal'); + const modal = document.querySelector(target); + const oldBranchName = el.getAttribute('data-old-branch-name'); + modal.querySelector('input[name=from]').value = oldBranchName; - const oldBranchName = $(this).attr('data-old-branch-name'); - $modal.find('input[name=from]').val(oldBranchName); + // display the warning that the branch which is chosen is the default branch + const warn = modal.querySelector('.default-branch-warning'); + toggleElem(warn, el.getAttribute('data-is-default-branch') === 'true'); - // display the warning that the branch which is chosen is the default branch - const $warn = $modal.find('.default-branch-warning'); - toggleElem($warn, $(this).attr('data-is-default-branch') === 'true'); - - const $text = $modal.find('[data-rename-branch-to]'); - $text.text($text.attr('data-rename-branch-to').replace('%s', oldBranchName)); - }); + const text = modal.querySelector('[data-rename-branch-to]'); + text.textContent = text.getAttribute('data-rename-branch-to').replace('%s', oldBranchName); + }); + } } diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js index 306f38829f..63da5f2039 100644 --- a/web_src/js/features/repo-code.js +++ b/web_src/js/features/repo-code.js @@ -16,48 +16,52 @@ function changeHash(hash) { } } -function selectRange($list, $select, $from) { - $list.removeClass('active'); +function isBlame() { + return Boolean(document.querySelector('div.blame')); +} + +function getLineEls() { + return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`); +} + +function selectRange($linesEls, $selectionEndEl, $selectionStartEls) { + for (const el of $linesEls) { + el.closest('tr').classList.remove('active'); + } // add hashchange to permalink - const $refInNewIssue = $('a.ref-in-new-issue'); - const $copyPermalink = $('a.copy-line-permalink'); - const $viewGitBlame = $('a.view_git_blame'); + const refInNewIssue = document.querySelector('a.ref-in-new-issue'); + const copyPermalink = document.querySelector('a.copy-line-permalink'); + const viewGitBlame = document.querySelector('a.view_git_blame'); const updateIssueHref = function (anchor) { - if ($refInNewIssue.length === 0) { - return; - } - const urlIssueNew = $refInNewIssue.attr('data-url-issue-new'); - const urlParamBodyLink = $refInNewIssue.attr('data-url-param-body-link'); + if (!refInNewIssue) return; + const urlIssueNew = refInNewIssue.getAttribute('data-url-issue-new'); + const urlParamBodyLink = refInNewIssue.getAttribute('data-url-param-body-link'); const issueContent = `${toAbsoluteUrl(urlParamBodyLink)}#${anchor}`; // the default content for issue body - $refInNewIssue.attr('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`); + refInNewIssue.setAttribute('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`); }; const updateViewGitBlameFragment = function (anchor) { - if ($viewGitBlame.length === 0) { - return; - } - let href = $viewGitBlame.attr('href'); + if (!viewGitBlame) return; + let href = viewGitBlame.getAttribute('href'); href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`; if (anchor.length !== 0) { href = `${href}#${anchor}`; } - $viewGitBlame.attr('href', href); + viewGitBlame.setAttribute('href', href); }; - const updateCopyPermalinkUrl = function(anchor) { - if ($copyPermalink.length === 0) { - return; - } - let link = $copyPermalink.attr('data-url'); + const updateCopyPermalinkUrl = function (anchor) { + if (!copyPermalink) return; + let link = copyPermalink.getAttribute('data-url'); link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`; - $copyPermalink.attr('data-url', link); + copyPermalink.setAttribute('data-url', link); }; - if ($from) { - let a = parseInt($select.attr('rel').slice(1)); - let b = parseInt($from.attr('rel').slice(1)); + if ($selectionStartEls) { + let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1)); + let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1)); let c; if (a !== b) { if (a > b) { @@ -69,7 +73,9 @@ function selectRange($list, $select, $from) { for (let i = a; i <= b; i++) { classes.push(`[rel=L${i}]`); } - $list.filter(classes.join(',')).addClass('active'); + $linesEls.filter(classes.join(',')).each(function () { + this.closest('tr').classList.add('active'); + }); changeHash(`#L${a}-L${b}`); updateIssueHref(`L${a}-L${b}`); @@ -78,12 +84,12 @@ function selectRange($list, $select, $from) { return; } } - $select.addClass('active'); - changeHash(`#${$select.attr('rel')}`); + $selectionEndEl[0].closest('tr').classList.add('active'); + changeHash(`#${$selectionEndEl[0].getAttribute('rel')}`); - updateIssueHref($select.attr('rel')); - updateViewGitBlameFragment($select.attr('rel')); - updateCopyPermalinkUrl($select.attr('rel')); + updateIssueHref($selectionEndEl[0].getAttribute('rel')); + updateViewGitBlameFragment($selectionEndEl[0].getAttribute('rel')); + updateCopyPermalinkUrl($selectionEndEl[0].getAttribute('rel')); } function showLineButton() { @@ -96,10 +102,10 @@ function showLineButton() { } // find active row and add button - const tr = document.querySelector('.code-view td.lines-code.active').closest('tr'); - const td = tr.querySelector('td'); + const tr = document.querySelector('.code-view tr.active'); + const td = tr.querySelector('td.lines-num'); const btn = document.createElement('button'); - btn.classList.add('code-line-button'); + btn.classList.add('code-line-button', 'ui', 'basic', 'button'); btn.innerHTML = svg('octicon-kebab-horizontal'); td.prepend(btn); @@ -116,21 +122,25 @@ function showLineButton() { tippy.popper.addEventListener('click', () => { tippy.hide(); }, {once: true}); - } + }, }); } export function initRepoCodeView() { if ($('.code-view .lines-num').length > 0) { $(document).on('click', '.lines-num span', function (e) { - const $select = $(this); - let $list; - if ($('div.blame').length) { - $list = $('.code-view td.lines-code.blame-code'); - } else { - $list = $('.code-view td.lines-code'); + const linesEls = getLineEls(); + const selectedEls = Array.from(linesEls).filter((el) => { + return el.matches(`[rel=${this.getAttribute('id')}]`); + }); + + let from; + if (e.shiftKey) { + from = Array.from(linesEls).filter((el) => { + return el.closest('tr').classList.contains('active'); + }); } - selectRange($list, $list.filter(`[rel=${$select.attr('id')}]`), (e.shiftKey ? $list.filter('.active').eq(0) : null)); + selectRange($(linesEls), $(selectedEls), from ? $(from) : null); if (window.getSelection) { window.getSelection().removeAllRanges(); @@ -138,28 +148,20 @@ export function initRepoCodeView() { document.selection.empty(); } - // show code view menu marker (don't show in blame page) - if ($('div.blame').length === 0) { - showLineButton(); - } + showLineButton(); }); $(window).on('hashchange', () => { let m = window.location.hash.match(rangeAnchorRegex); - let $list; - if ($('div.blame').length) { - $list = $('.code-view td.lines-code.blame-code'); - } else { - $list = $('.code-view td.lines-code'); - } + const $linesEls = $(getLineEls()); let $first; if (m) { - $first = $list.filter(`[rel=${m[1]}]`); + $first = $linesEls.filter(`[rel=${m[1]}]`); if ($first.length) { - selectRange($list, $first, $list.filter(`[rel=${m[2]}]`)); + selectRange($linesEls, $first, $linesEls.filter(`[rel=${m[2]}]`)); // show code view menu marker (don't show in blame page) - if ($('div.blame').length === 0) { + if (!isBlame()) { showLineButton(); } @@ -169,12 +171,12 @@ export function initRepoCodeView() { } m = window.location.hash.match(singleAnchorRegex); if (m) { - $first = $list.filter(`[rel=L${m[2]}]`); + $first = $linesEls.filter(`[rel=L${m[2]}]`); if ($first.length) { - selectRange($list, $first); + selectRange($linesEls, $first); // show code view menu marker (don't show in blame page) - if ($('div.blame').length === 0) { + if (!isBlame()) { showLineButton(); } @@ -186,15 +188,7 @@ export function initRepoCodeView() { $(document).on('click', '.fold-file', ({currentTarget}) => { invertFileFolding(currentTarget.closest('.file-content'), currentTarget); }); - $(document).on('click', '.code-expander-button', async ({currentTarget}) => { - const url = currentTarget.getAttribute('data-url'); - const query = currentTarget.getAttribute('data-query'); - const anchor = currentTarget.getAttribute('data-anchor'); - if (!url) return; - const blob = await $.get(`${url}?${query}&anchor=${anchor}`); - currentTarget.closest('tr').outerHTML = blob; - }); - $(document).on('click', '.copy-line-permalink', async (e) => { - await clippie(toAbsoluteUrl(e.currentTarget.getAttribute('data-url'))); + $(document).on('click', '.copy-line-permalink', async ({currentTarget}) => { + await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url'))); }); } diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js index 76b34d2077..f61ea08a42 100644 --- a/web_src/js/features/repo-commit.js +++ b/web_src/js/features/repo-commit.js @@ -1,72 +1,27 @@ -import $ from 'jquery'; import {createTippy} from '../modules/tippy.js'; import {toggleElem} from '../utils/dom.js'; -const {csrfToken} = window.config; - export function initRepoEllipsisButton() { - $('.js-toggle-commit-body').on('click', function (e) { - e.preventDefault(); - const expanded = $(this).attr('aria-expanded') === 'true'; - toggleElem($(this).parent().find('.commit-body')); - $(this).attr('aria-expanded', String(!expanded)); - }); -} - -export function initRepoCommitLastCommitLoader() { - const entryMap = {}; - - const entries = $('table#repo-files-table tr.notready') - .map((_, v) => { - entryMap[$(v).attr('data-entryname')] = $(v); - return $(v).attr('data-entryname'); - }) - .get(); - - if (entries.length === 0) { - return; - } - - const lastCommitLoaderURL = $('table#repo-files-table').data('lastCommitLoaderUrl'); - - if (entries.length > 200) { - $.post(lastCommitLoaderURL, { - _csrf: csrfToken, - }, (data) => { - $('table#repo-files-table').replaceWith(data); + for (const button of document.querySelectorAll('.js-toggle-commit-body')) { + button.addEventListener('click', function (e) { + e.preventDefault(); + const expanded = this.getAttribute('aria-expanded') === 'true'; + toggleElem(this.parentElement.querySelector('.commit-body')); + this.setAttribute('aria-expanded', String(!expanded)); }); - return; } - - $.post(lastCommitLoaderURL, { - _csrf: csrfToken, - 'f': entries, - }, (data) => { - $(data).find('tr').each((_, row) => { - if (row.className === 'commit-list') { - $('table#repo-files-table .commit-list').replaceWith(row); - return; - } - // there are other <tr> rows in response (eg: <tr class="has-parent">) - // at the moment only the "data-entryname" rows should be processed - const entryName = $(row).attr('data-entryname'); - if (entryName) { - entryMap[entryName].replaceWith(row); - } - }); - }); } export function initCommitStatuses() { - $('[data-tippy="commit-statuses"]').each(function () { - const top = $('.repository.file.list').length > 0 || $('.repository.diff').length > 0; + for (const element of document.querySelectorAll('[data-tippy="commit-statuses"]')) { + const top = document.querySelector('.repository.file.list') || document.querySelector('.repository.diff'); - createTippy(this, { - content: this.nextElementSibling, + createTippy(element, { + content: element.nextElementSibling, placement: top ? 'top-start' : 'bottom-start', interactive: true, role: 'dialog', theme: 'box-with-header', }); - }); + } } diff --git a/web_src/js/features/repo-common.js b/web_src/js/features/repo-common.js index 3573e4d50b..b750addb07 100644 --- a/web_src/js/features/repo-common.js +++ b/web_src/js/features/repo-common.js @@ -1,44 +1,42 @@ import $ from 'jquery'; import {hideElem, showElem} from '../utils/dom.js'; +import {POST} from '../modules/fetch.js'; -const {csrfToken} = window.config; +async function getArchive($target, url, first) { + const dropdownBtn = $target[0].closest('.ui.dropdown.button') ?? $target[0].closest('.ui.dropdown.btn'); -function getArchive($target, url, first) { - $.ajax({ - url, - type: 'POST', - data: { - _csrf: csrfToken, - }, - complete(xhr) { - if (xhr.status === 200) { - if (!xhr.responseJSON) { - // XXX Shouldn't happen? - $target.closest('.dropdown').children('i').removeClass('loading'); - return; - } - - if (!xhr.responseJSON.complete) { - $target.closest('.dropdown').children('i').addClass('loading'); - // Wait for only three quarters of a second initially, in case it's - // quickly archived. - setTimeout(() => { - getArchive($target, url, false); - }, first ? 750 : 2000); - } else { - // We don't need to continue checking. - $target.closest('.dropdown').children('i').removeClass('loading'); - window.location.href = url; - } + try { + dropdownBtn.classList.add('is-loading'); + const response = await POST(url); + if (response.status === 200) { + const data = await response.json(); + if (!data) { + // XXX Shouldn't happen? + dropdownBtn.classList.remove('is-loading'); + return; } - }, - }); + + if (!data.complete) { + // Wait for only three quarters of a second initially, in case it's + // quickly archived. + setTimeout(() => { + getArchive($target, url, false); + }, first ? 750 : 2000); + } else { + // We don't need to continue checking. + dropdownBtn.classList.remove('is-loading'); + window.location.href = url; + } + } + } catch { + dropdownBtn.classList.remove('is-loading'); + } } export function initRepoArchiveLinks() { $('.archive-link').on('click', function (event) { event.preventDefault(); - const url = $(this).attr('href'); + const url = this.getAttribute('href'); if (!url) return; getArchive($(event.target), url, true); }); @@ -80,14 +78,16 @@ export function initRepoCommonBranchOrTagDropdown(selector) { export function initRepoCommonFilterSearchDropdown(selector) { const $dropdown = $(selector); + if (!$dropdown.length) return; + $dropdown.dropdown({ fullTextSearch: 'exact', selectOnKeydown: false, onChange(_text, _value, $choice) { - if ($choice.attr('data-url')) { - window.location.href = $choice.attr('data-url'); + if ($choice[0].getAttribute('data-url')) { + window.location.href = $choice[0].getAttribute('data-url'); } }, - message: {noResults: $dropdown.attr('data-no-results')}, + message: {noResults: $dropdown[0].getAttribute('data-no-results')}, }); } diff --git a/web_src/js/features/repo-diff-commit.js b/web_src/js/features/repo-diff-commit.js index 3d4f0f677a..aa7fc38360 100644 --- a/web_src/js/features/repo-diff-commit.js +++ b/web_src/js/features/repo-diff-commit.js @@ -35,11 +35,11 @@ function addBranches(area, branches, defaultBranch) { function addLink(parent, href, text, tooltip) { const link = document.createElement('a'); - link.classList.add('muted', 'gt-px-2'); + link.classList.add('muted', 'tw-px-1'); link.href = href; link.textContent = text; if (tooltip) { - link.classList.add('gt-border-secondary', 'gt-rounded'); + link.classList.add('tw-border', 'tw-border-secondary', 'tw-rounded'); link.setAttribute('data-tooltip-content', tooltip); } parent.append(link); diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js index eeb80e91b2..b2e8ec4866 100644 --- a/web_src/js/features/repo-diff.js +++ b/web_src/js/features/repo-diff.js @@ -7,38 +7,46 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js'; import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js'; import {initImageDiff} from './imagediff.js'; import {showErrorToast} from '../modules/toast.js'; -import {submitEventSubmitter} from '../utils/dom.js'; +import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js'; +import {POST, GET} from '../modules/fetch.js'; -const {csrfToken, pageData, i18n} = window.config; +const {pageData, i18n} = window.config; function initRepoDiffReviewButton() { - const $reviewBox = $('#review-box'); - const $counter = $reviewBox.find('.review-comments-counter'); + const reviewBox = document.getElementById('review-box'); + if (!reviewBox) return; + + const counter = reviewBox.querySelector('.review-comments-counter'); + if (!counter) return; $(document).on('click', 'button[name="pending_review"]', (e) => { const $form = $(e.target).closest('form'); // Watch for the form's submit event. $form.on('submit', () => { - const num = parseInt($counter.attr('data-pending-comment-number')) + 1 || 1; - $counter.attr('data-pending-comment-number', num); - $counter.text(num); - // Force the browser to reflow the DOM. This is to ensure that the browser replay the animation - $reviewBox.removeClass('pulse'); - $reviewBox.width(); - $reviewBox.addClass('pulse'); + const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1; + counter.setAttribute('data-pending-comment-number', num); + counter.textContent = num; + + reviewBox.classList.remove('pulse'); + requestAnimationFrame(() => { + reviewBox.classList.add('pulse'); + }); }); }); } function initRepoDiffFileViewToggle() { $('.file-view-toggle').on('click', function () { - const $this = $(this); - $this.parent().children().removeClass('active'); - $this.addClass('active'); + for (const el of queryElemSiblings(this)) { + el.classList.remove('active'); + } + this.classList.add('active'); - const $target = $($this.data('toggle-selector')); - $target.parent().children().addClass('gt-hidden'); - $target.removeClass('gt-hidden'); + const target = document.querySelector(this.getAttribute('data-toggle-selector')); + if (!target) return; + + hideElem(queryElemSiblings(target)); + showElem(target); }); } @@ -47,38 +55,44 @@ function initRepoDiffConversationForm() { e.preventDefault(); const $form = $(e.target); - const $textArea = $form.find('textarea'); - if (!validateTextareaNonEmpty($textArea)) { + const textArea = e.target.querySelector('textarea'); + if (!validateTextareaNonEmpty(textArea)) { return; } - if ($form.hasClass('is-loading')) return; + if (e.target.classList.contains('is-loading')) return; try { - $form.addClass('is-loading'); + e.target.classList.add('is-loading'); const formData = new FormData($form[0]); // if the form is submitted by a button, append the button's name and value to the form data - const submitter = submitEventSubmitter(e.originalEvent); + const submitter = submitEventSubmitter(e); const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit'); if (isSubmittedByButton && submitter.name) { formData.append(submitter.name, submitter.value); } - const formDataString = String(new URLSearchParams(formData)); - const $newConversationHolder = $(await $.post($form.attr('action'), formDataString)); + + const response = await POST(e.target.getAttribute('action'), {data: formData}); + const $newConversationHolder = $(await response.text()); const {path, side, idx} = $newConversationHolder.data(); $form.closest('.conversation-holder').replaceWith($newConversationHolder); + let selector; if ($form.closest('tr').data('line-type') === 'same') { - $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).addClass('gt-invisible'); + selector = `[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`; } else { - $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).addClass('gt-invisible'); + selector = `[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`; + } + for (const el of document.querySelectorAll(selector)) { + el.classList.add('tw-invisible'); } $newConversationHolder.find('.dropdown').dropdown(); initCompReactionSelector($newConversationHolder); - } catch { // here the caught error might be a jQuery AJAX error (thrown by await $.post), which is not good to use for error message handling + } catch (error) { + console.error('Error:', error); showErrorToast(i18n.network_error); } finally { - $form.removeClass('is-loading'); + e.target.classList.remove('is-loading'); } }); @@ -89,15 +103,20 @@ function initRepoDiffConversationForm() { const action = $(this).data('action'); const url = $(this).data('update-url'); - const data = await $.post(url, {_csrf: csrfToken, origin, action, comment_id}); + try { + const response = await POST(url, {data: new URLSearchParams({origin, action, comment_id})}); + const data = await response.text(); - if ($(this).closest('.conversation-holder').length) { - const conversation = $(data); - $(this).closest('.conversation-holder').replaceWith(conversation); - conversation.find('.dropdown').dropdown(); - initCompReactionSelector(conversation); - } else { - window.location.reload(); + if ($(this).closest('.conversation-holder').length) { + const $conversation = $(data); + $(this).closest('.conversation-holder').replaceWith($conversation); + $conversation.find('.dropdown').dropdown(); + initCompReactionSelector($conversation); + } else { + window.location.reload(); + } + } catch (error) { + console.error('Error:', error); } }); } @@ -106,20 +125,20 @@ export function initRepoDiffConversationNav() { // Previous/Next code review conversation $(document).on('click', '.previous-conversation', (e) => { const $conversation = $(e.currentTarget).closest('.comment-code-cloud'); - const $conversations = $('.comment-code-cloud:not(.gt-hidden)'); + const $conversations = $('.comment-code-cloud:not(.tw-hidden)'); const index = $conversations.index($conversation); const previousIndex = index > 0 ? index - 1 : $conversations.length - 1; const $previousConversation = $conversations.eq(previousIndex); - const anchor = $previousConversation.find('.comment').first().attr('id'); + const anchor = $previousConversation.find('.comment').first()[0].getAttribute('id'); window.location.href = `#${anchor}`; }); $(document).on('click', '.next-conversation', (e) => { const $conversation = $(e.currentTarget).closest('.comment-code-cloud'); - const $conversations = $('.comment-code-cloud:not(.gt-hidden)'); + const $conversations = $('.comment-code-cloud:not(.tw-hidden)'); const index = $conversations.index($conversation); const nextIndex = index < $conversations.length - 1 ? index + 1 : 0; const $nextConversation = $conversations.eq(nextIndex); - const anchor = $nextConversation.find('.comment').first().attr('id'); + const anchor = $nextConversation.find('.comment').first()[0].getAttribute('id'); window.location.href = `#${anchor}`; }); } @@ -132,18 +151,18 @@ function onShowMoreFiles() { initImageDiff(); } -export function loadMoreFiles(url) { - const $target = $('a#diff-show-more-files'); - if ($target.hasClass('disabled') || pageData.diffFileInfo.isLoadingNewData) { +export async function loadMoreFiles(url) { + const target = document.querySelector('a#diff-show-more-files'); + if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) { return; } pageData.diffFileInfo.isLoadingNewData = true; - $target.addClass('disabled'); - $.ajax({ - type: 'GET', - url, - }).done((resp) => { + target?.classList.add('disabled'); + + try { + const response = await GET(url); + const resp = await response.text(); const $resp = $(resp); // the response is a full HTML page, we need to extract the relevant contents: // 1. append the newly loaded file list items to the existing list @@ -152,52 +171,55 @@ export function loadMoreFiles(url) { $('body').append($resp.find('script#diff-data-script')); onShowMoreFiles(); - }).always(() => { - $target.removeClass('disabled'); + } catch (error) { + console.error('Error:', error); + showErrorToast('An error occurred while loading more files.'); + } finally { + target?.classList.remove('disabled'); pageData.diffFileInfo.isLoadingNewData = false; - }); + } } function initRepoDiffShowMore() { $(document).on('click', 'a#diff-show-more-files', (e) => { e.preventDefault(); - const $target = $(e.target); - const linkLoadMore = $target.attr('data-href'); + const linkLoadMore = e.target.getAttribute('data-href'); loadMoreFiles(linkLoadMore); }); - $(document).on('click', 'a.diff-load-button', (e) => { + $(document).on('click', 'a.diff-load-button', async (e) => { e.preventDefault(); const $target = $(e.target); - if ($target.hasClass('disabled')) { + if (e.target.classList.contains('disabled')) { return; } - $target.addClass('disabled'); + e.target.classList.add('disabled'); const url = $target.data('href'); - $.ajax({ - type: 'GET', - url, - }).done((resp) => { + + try { + const response = await GET(url); + const resp = await response.text(); + if (!resp) { - $target.removeClass('disabled'); return; } $target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children()); onShowMoreFiles(); - }).fail(() => { - $target.removeClass('disabled'); - }); + } catch (error) { + console.error('Error:', error); + } finally { + e.target.classList.remove('disabled'); + } }); } export function initRepoDiffView() { initRepoDiffConversationForm(); - const diffFileList = $('#diff-file-list'); - if (diffFileList.length === 0) return; + if (!$('#diff-file-list').length) return; initDiffFileTree(); initDiffCommitSelect(); initRepoDiffShowMore(); diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js index f00f817223..01dc4b95aa 100644 --- a/web_src/js/features/repo-editor.js +++ b/web_src/js/features/repo-editor.js @@ -4,60 +4,44 @@ import {createCodeEditor} from './codeeditor.js'; import {hideElem, showElem} from '../utils/dom.js'; import {initMarkupContent} from '../markup/content.js'; import {attachRefIssueContextPopup} from './contextpopup.js'; - -const {csrfToken} = window.config; +import {POST} from '../modules/fetch.js'; function initEditPreviewTab($form) { const $tabMenu = $form.find('.tabular.menu'); $tabMenu.find('.item').tab(); const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`); if ($previewTab.length) { - $previewTab.on('click', function () { + $previewTab.on('click', async function () { const $this = $(this); let context = `${$this.data('context')}/`; const mode = $this.data('markup-mode') || 'comment'; - const treePathEl = $form.find('input#tree_path'); - if (treePathEl.length > 0) { - context += treePathEl.val(); + const $treePathEl = $form.find('input#tree_path'); + if ($treePathEl.length > 0) { + context += $treePathEl.val(); } context = context.substring(0, context.lastIndexOf('/')); - $.post($this.data('url'), { - _csrf: csrfToken, - mode, - context, - text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(), - file_path: treePathEl.val(), - }, (data) => { + + const formData = new FormData(); + formData.append('mode', mode); + formData.append('context', context); + formData.append('text', $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val()); + formData.append('file_path', $treePathEl.val()); + try { + const response = await POST($this.data('url'), {data: formData}); + const data = await response.text(); const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`); renderPreviewPanelContent($previewPanel, data); - }); + } catch (error) { + console.error('Error:', error); + } }); } } -function initEditDiffTab($form) { - const $tabMenu = $form.find('.tabular.menu'); - $tabMenu.find('.item').tab(); - $tabMenu.find(`.item[data-tab="${$tabMenu.data('diff')}"]`).on('click', function () { - const $this = $(this); - $.post($this.data('url'), { - _csrf: csrfToken, - context: $this.data('context'), - content: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(), - }, (data) => { - const $diffPreviewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('diff')}"]`); - $diffPreviewPanel.html(data); - }); - }); -} - function initEditorForm() { - if ($('.repository .edit.form').length === 0) { - return; - } - - initEditPreviewTab($('.repository .edit.form')); - initEditDiffTab($('.repository .edit.form')); + const $form = $('.repository .edit.form'); + if (!$form) return; + initEditPreviewTab($form); } function getCursorPosition($e) { @@ -80,23 +64,23 @@ export function initRepoEditor() { $('.js-quick-pull-choice-option').on('change', function () { if ($(this).val() === 'commit-to-new-branch') { - showElem($('.quick-pull-branch-name')); - $('.quick-pull-branch-name input').prop('required', true); + showElem('.quick-pull-branch-name'); + document.querySelector('.quick-pull-branch-name input').required = true; } else { - hideElem($('.quick-pull-branch-name')); - $('.quick-pull-branch-name input').prop('required', false); + hideElem('.quick-pull-branch-name'); + document.querySelector('.quick-pull-branch-name input').required = false; } - $('#commit-button').text($(this).attr('button_text')); + $('#commit-button').text(this.getAttribute('button_text')); }); const joinTreePath = ($fileNameEl) => { const parts = []; $('.breadcrumb span.section').each(function () { - const element = $(this); - if (element.find('a').length) { - parts.push(element.find('a').text()); + const $element = $(this); + if ($element.find('a').length) { + parts.push($element.find('a').text()); } else { - parts.push(element.text()); + parts.push($element.text()); } }); if ($fileNameEl.val()) parts.push($fileNameEl.val()); @@ -149,13 +133,13 @@ export function initRepoEditor() { // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // to enable or disable the commit button - const $commitButton = $('#commit-button'); + const commitButton = document.getElementById('commit-button'); const $editForm = $('.ui.edit.form'); const dirtyFileClass = 'dirty-file'; // Disabling the button at the start if ($('input[name="page_has_posted"]').val() !== 'true') { - $commitButton.prop('disabled', true); + commitButton.disabled = true; } // Registering a custom listener for the file path and the file content @@ -163,9 +147,9 @@ export function initRepoEditor() { silent: true, dirtyClass: dirtyFileClass, fieldSelector: ':input:not(.commit-form-wrapper :input)', - change() { - const dirty = $(this).hasClass(dirtyFileClass); - $commitButton.prop('disabled', !dirty); + change($form) { + const dirty = $form[0]?.classList.contains(dirtyFileClass); + commitButton.disabled = !dirty; }, }); @@ -177,15 +161,15 @@ export function initRepoEditor() { editor.setValue(value); } - $commitButton.on('click', (event) => { + commitButton?.addEventListener('click', (e) => { // A modal which asks if an empty file should be committed - if ($editArea.val().length === 0) { + if (!$editArea.val()) { $('#edit-empty-content-modal').modal({ onApprove() { $('.edit.form').trigger('submit'); }, }).modal('show'); - event.preventDefault(); + e.preventDefault(); } }); })(); @@ -195,6 +179,6 @@ export function renderPreviewPanelContent($panelPreviewer, data) { $panelPreviewer.html(data); initMarkupContent(); - const refIssues = $panelPreviewer.find('p .ref-issue'); - attachRefIssueContextPopup(refIssues); + const $refIssues = $panelPreviewer.find('p .ref-issue'); + attachRefIssueContextPopup($refIssues); } diff --git a/web_src/js/features/repo-findfile.js b/web_src/js/features/repo-findfile.js index 158732acc2..cff5068a1e 100644 --- a/web_src/js/features/repo-findfile.js +++ b/web_src/js/features/repo-findfile.js @@ -1,13 +1,11 @@ -import $ from 'jquery'; import {svg} from '../svg.js'; import {toggleElem} from '../utils/dom.js'; import {pathEscapeSegments} from '../utils/url.js'; - -const {csrf} = window.config; +import {GET} from '../modules/fetch.js'; const threshold = 50; let files = []; -let $repoFindFileInput, $repoFindFileTableBody, $repoFindFileNoResult; +let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult; // return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...] // res[even] is unmatched, res[odd] is matched, see unit tests for examples @@ -74,46 +72,46 @@ export function filterRepoFilesWeighted(files, filter) { } function filterRepoFiles(filter) { - const treeLink = $repoFindFileInput.attr('data-url-tree-link'); - $repoFindFileTableBody.empty(); + const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); + repoFindFileTableBody.innerHTML = ''; const filterResult = filterRepoFilesWeighted(files, filter); - const tmplRow = `<tr><td><a></a></td></tr>`; - toggleElem($repoFindFileNoResult, filterResult.length === 0); + toggleElem(repoFindFileNoResult, !filterResult.length); for (const r of filterResult) { - const $row = $(tmplRow); - const $a = $row.find('a'); - $a.attr('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`); - const $octiconFile = $(svg('octicon-file')).addClass('gt-mr-3'); - $a.append($octiconFile); - // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz'] - // the matchResult[odd] is matched and highlighted to red. - for (let j = 0; j < r.matchResult.length; j++) { - if (!r.matchResult[j]) continue; - const $span = $('<span>').text(r.matchResult[j]); - if (j % 2 === 1) $span.addClass('ui text red'); - $a.append($span); + const row = document.createElement('tr'); + const cell = document.createElement('td'); + const a = document.createElement('a'); + a.setAttribute('href', `${treeLink}/${pathEscapeSegments(r.matchResult.join(''))}`); + a.innerHTML = svg('octicon-file', 16, 'tw-mr-2'); + row.append(cell); + cell.append(a); + for (const [index, part] of r.matchResult.entries()) { + const span = document.createElement('span'); + // safely escape by using textContent + span.textContent = part; + // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz'] + // the matchResult[odd] is matched and highlighted to red. + if (index % 2 === 1) span.classList.add('ui', 'text', 'red'); + a.append(span); } - $repoFindFileTableBody.append($row); + repoFindFileTableBody.append(row); } } async function loadRepoFiles() { - files = await $.ajax({ - url: $repoFindFileInput.attr('data-url-data-link'), - headers: {'X-Csrf-Token': csrf} - }); - filterRepoFiles($repoFindFileInput.val()); + const response = await GET(repoFindFileInput.getAttribute('data-url-data-link')); + files = await response.json(); + filterRepoFiles(repoFindFileInput.value); } export function initFindFileInRepo() { - $repoFindFileInput = $('#repo-file-find-input'); - if (!$repoFindFileInput.length) return; + repoFindFileInput = document.getElementById('repo-file-find-input'); + if (!repoFindFileInput) return; - $repoFindFileTableBody = $('#repo-find-file-table tbody'); - $repoFindFileNoResult = $('#repo-find-file-no-result'); - $repoFindFileInput.on('input', () => filterRepoFiles($repoFindFileInput.val())); + repoFindFileTableBody = document.querySelector('#repo-find-file-table tbody'); + repoFindFileNoResult = document.getElementById('repo-find-file-no-result'); + repoFindFileInput.addEventListener('input', () => filterRepoFiles(repoFindFileInput.value)); loadRepoFiles(); } diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js index e445ae1103..0086b92021 100644 --- a/web_src/js/features/repo-graph.js +++ b/web_src/js/features/repo-graph.js @@ -1,13 +1,16 @@ import $ from 'jquery'; +import {hideElem, showElem} from '../utils/dom.js'; +import {GET} from '../modules/fetch.js'; export function initRepoGraphGit() { const graphContainer = document.getElementById('git-graph-container'); if (!graphContainer) return; - $('#flow-color-monochrome').on('click', () => { - $('#flow-color-monochrome').addClass('active'); - $('#flow-color-colored').removeClass('active'); - $('#git-graph-container').removeClass('colored').addClass('monochrome'); + document.getElementById('flow-color-monochrome')?.addEventListener('click', () => { + document.getElementById('flow-color-monochrome').classList.add('active'); + document.getElementById('flow-color-colored')?.classList.remove('active'); + graphContainer.classList.remove('colored'); + graphContainer.classList.add('monochrome'); const params = new URLSearchParams(window.location.search); params.set('mode', 'monochrome'); const queryString = params.toString(); @@ -16,29 +19,31 @@ export function initRepoGraphGit() { } else { window.history.replaceState({}, '', window.location.pathname); } - $('.pagination a').each((_, that) => { - const href = $(that).attr('href'); - if (!href) return; + for (const link of document.querySelectorAll('.pagination a')) { + const href = link.getAttribute('href'); + if (!href) continue; const url = new URL(href, window.location); const params = url.searchParams; params.set('mode', 'monochrome'); url.search = `?${params.toString()}`; - $(that).attr('href', url.href); - }); + link.setAttribute('href', url.href); + } }); - $('#flow-color-colored').on('click', () => { - $('#flow-color-colored').addClass('active'); - $('#flow-color-monochrome').removeClass('active'); - $('#git-graph-container').addClass('colored').removeClass('monochrome'); - $('.pagination a').each((_, that) => { - const href = $(that).attr('href'); - if (!href) return; + + document.getElementById('flow-color-colored')?.addEventListener('click', () => { + document.getElementById('flow-color-colored').classList.add('active'); + document.getElementById('flow-color-monochrome')?.classList.remove('active'); + graphContainer.classList.add('colored'); + graphContainer.classList.remove('monochrome'); + for (const link of document.querySelectorAll('.pagination a')) { + const href = link.getAttribute('href'); + if (!href) continue; const url = new URL(href, window.location); const params = url.searchParams; params.delete('mode'); url.search = `?${params.toString()}`; - $(that).attr('href', url.href); - }); + link.setAttribute('href', url.href); + } const params = new URLSearchParams(window.location.search); params.delete('mode'); const queryString = params.toString(); @@ -55,18 +60,21 @@ export function initRepoGraphGit() { const ajaxUrl = new URL(url); ajaxUrl.searchParams.set('div-only', 'true'); window.history.replaceState({}, '', queryString ? `?${queryString}` : window.location.pathname); - $('#pagination').empty(); - $('#rel-container').addClass('gt-hidden'); - $('#rev-container').addClass('gt-hidden'); - $('#loading-indicator').removeClass('gt-hidden'); + document.getElementById('pagination').innerHTML = ''; + hideElem('#rel-container'); + hideElem('#rev-container'); + showElem('#loading-indicator'); (async () => { - const div = $(await $.ajax(String(ajaxUrl))); - $('#pagination').html(div.find('#pagination').html()); - $('#rel-container').html(div.find('#rel-container').html()); - $('#rev-container').html(div.find('#rev-container').html()); - $('#loading-indicator').addClass('gt-hidden'); - $('#rel-container').removeClass('gt-hidden'); - $('#rev-container').removeClass('gt-hidden'); + const response = await GET(String(ajaxUrl)); + const html = await response.text(); + const div = document.createElement('div'); + div.innerHTML = html; + document.getElementById('pagination').innerHTML = div.getElementById('pagination').innerHTML; + document.getElementById('rel-container').innerHTML = div.getElementById('rel-container').innerHTML; + document.getElementById('rev-container').innerHTML = div.getElementById('rev-container').innerHTML; + hideElem('#loading-indicator'); + showElem('#rel-container'); + showElem('#rev-container'); })(); }; const dropdownSelected = params.getAll('branch'); @@ -74,8 +82,9 @@ export function initRepoGraphGit() { dropdownSelected.splice(0, 0, '...flow-hide-pr-refs'); } - $('#flow-select-refs-dropdown').dropdown('set selected', dropdownSelected); - $('#flow-select-refs-dropdown').dropdown({ + const flowSelectRefsDropdown = document.getElementById('flow-select-refs-dropdown'); + $(flowSelectRefsDropdown).dropdown('set selected', dropdownSelected); + $(flowSelectRefsDropdown).dropdown({ clearable: true, fullTextSeach: 'exact', onRemove(toRemove) { @@ -101,36 +110,46 @@ export function initRepoGraphGit() { updateGraph(); }, }); - $('#git-graph-container').on('mouseenter', '#rev-list li', (e) => { - const flow = $(e.currentTarget).data('flow'); - if (flow === 0) return; - $(`#flow-${flow}`).addClass('highlight'); - $(e.currentTarget).addClass('hover'); - $(`#rev-list li[data-flow='${flow}']`).addClass('highlight'); + + graphContainer.addEventListener('mouseenter', (e) => { + if (e.target.matches('#rev-list li')) { + const flow = e.target.getAttribute('data-flow'); + if (flow === '0') return; + document.getElementById(`flow-${flow}`)?.classList.add('highlight'); + e.target.classList.add('hover'); + for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) { + item.classList.add('highlight'); + } + } else if (e.target.matches('#rel-container .flow-group')) { + e.target.classList.add('highlight'); + const flow = e.target.getAttribute('data-flow'); + for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) { + item.classList.add('highlight'); + } + } else if (e.target.matches('#rel-container .flow-commit')) { + const rev = e.target.getAttribute('data-rev'); + document.querySelector(`#rev-list li#commit-${rev}`)?.classList.add('hover'); + } }); - $('#git-graph-container').on('mouseleave', '#rev-list li', (e) => { - const flow = $(e.currentTarget).data('flow'); - if (flow === 0) return; - $(`#flow-${flow}`).removeClass('highlight'); - $(e.currentTarget).removeClass('hover'); - $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight'); - }); - $('#git-graph-container').on('mouseenter', '#rel-container .flow-group', (e) => { - $(e.currentTarget).addClass('highlight'); - const flow = $(e.currentTarget).data('flow'); - $(`#rev-list li[data-flow='${flow}']`).addClass('highlight'); - }); - $('#git-graph-container').on('mouseleave', '#rel-container .flow-group', (e) => { - $(e.currentTarget).removeClass('highlight'); - const flow = $(e.currentTarget).data('flow'); - $(`#rev-list li[data-flow='${flow}']`).removeClass('highlight'); - }); - $('#git-graph-container').on('mouseenter', '#rel-container .flow-commit', (e) => { - const rev = $(e.currentTarget).data('rev'); - $(`#rev-list li#commit-${rev}`).addClass('hover'); - }); - $('#git-graph-container').on('mouseleave', '#rel-container .flow-commit', (e) => { - const rev = $(e.currentTarget).data('rev'); - $(`#rev-list li#commit-${rev}`).removeClass('hover'); + + graphContainer.addEventListener('mouseleave', (e) => { + if (e.target.matches('#rev-list li')) { + const flow = e.target.getAttribute('data-flow'); + if (flow === '0') return; + document.getElementById(`flow-${flow}`)?.classList.remove('highlight'); + e.target.classList.remove('hover'); + for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) { + item.classList.remove('highlight'); + } + } else if (e.target.matches('#rel-container .flow-group')) { + e.target.classList.remove('highlight'); + const flow = e.target.getAttribute('data-flow'); + for (const item of document.querySelectorAll(`#rev-list li[data-flow='${flow}']`)) { + item.classList.remove('highlight'); + } + } else if (e.target.matches('#rel-container .flow-commit')) { + const rev = e.target.getAttribute('data-rev'); + document.querySelector(`#rev-list li#commit-${rev}`)?.classList.remove('hover'); + } }); } diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index 3603fae2e9..6a5bce8268 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -1,82 +1,79 @@ import $ from 'jquery'; import {stripTags} from '../utils.js'; -import {hideElem, showElem} from '../utils/dom.js'; +import {hideElem, queryElemChildren, showElem} from '../utils/dom.js'; +import {POST} from '../modules/fetch.js'; +import {showErrorToast} from '../modules/toast.js'; -const {appSubUrl, csrfToken} = window.config; +const {appSubUrl} = window.config; export function initRepoTopicBar() { - const mgrBtn = $('#manage_topic'); - if (!mgrBtn.length) return; - const editDiv = $('#topic_edit'); - const viewDiv = $('#repo-topics'); - const saveBtn = $('#save_topic'); - const topicDropdown = $('#topic_edit .dropdown'); - const topicForm = editDiv; // the old logic, editDiv is topicForm - const topicDropdownSearch = topicDropdown.find('input.search'); - const topicPrompts = { - countPrompt: topicDropdown.attr('data-text-count-prompt'), - formatPrompt: topicDropdown.attr('data-text-format-prompt'), - }; + const mgrBtn = document.getElementById('manage_topic'); + if (!mgrBtn) return; - mgrBtn.on('click', () => { + const editDiv = document.getElementById('topic_edit'); + const viewDiv = document.getElementById('repo-topics'); + const topicDropdown = editDiv.querySelector('.ui.dropdown'); + let lastErrorToast; + + mgrBtn.addEventListener('click', () => { hideElem(viewDiv); showElem(editDiv); - topicDropdownSearch.focus(); + topicDropdown.querySelector('input.search').focus(); }); - $('#cancel_topic_edit').on('click', () => { + document.querySelector('#cancel_topic_edit').addEventListener('click', () => { + lastErrorToast?.hideToast(); hideElem(editDiv); showElem(viewDiv); mgrBtn.focus(); }); - saveBtn.on('click', () => { - const topics = $('input[name=topics]').val(); + document.getElementById('save_topic').addEventListener('click', async (e) => { + lastErrorToast?.hideToast(); + const topics = editDiv.querySelector('input[name=topics]').value; - $.post(saveBtn.attr('data-link'), { - _csrf: csrfToken, - topics - }, (_data, _textStatus, xhr) => { - if (xhr.responseJSON.status === 'ok') { - viewDiv.children('.topic').remove(); + const data = new FormData(); + data.append('topics', topics); + + const response = await POST(e.target.getAttribute('data-link'), {data}); + + if (response.ok) { + const responseData = await response.json(); + if (responseData.status === 'ok') { + queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove()); if (topics.length) { const topicArray = topics.split(','); topicArray.sort(); - for (let i = 0; i < topicArray.length; i++) { - const link = $('<a class="ui repo-topic large label topic gt-m-0"></a>'); - link.attr('href', `${appSubUrl}/explore/repos?q=${encodeURIComponent(topicArray[i])}&topic=1`); - link.text(topicArray[i]); - link.insertBefore(mgrBtn); // insert all new topics before manage button + for (const topic of topicArray) { + // it should match the code in repo/home.tmpl + const link = document.createElement('a'); + link.classList.add('repo-topic', 'ui', 'large', 'label'); + link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`; + link.textContent = topic; + mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button } } hideElem(editDiv); showElem(viewDiv); } - }).fail((xhr) => { - if (xhr.status === 422) { - if (xhr.responseJSON.invalidTopics.length > 0) { - topicPrompts.formatPrompt = xhr.responseJSON.message; - - const {invalidTopics} = xhr.responseJSON; - const topicLabels = topicDropdown.children('a.ui.label'); - - for (const [index, value] of topics.split(',').entries()) { - for (let i = 0; i < invalidTopics.length; i++) { - if (invalidTopics[i] === value) { - topicLabels.eq(index).removeClass('green').addClass('red'); - } - } + } else if (response.status === 422) { + // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save" + const responseData = await response.json(); + lastErrorToast = showErrorToast(responseData.message, {duration: 5000}); + if (responseData.invalidTopics.length > 0) { + const {invalidTopics} = responseData; + const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label'); + for (const [index, value] of topics.split(',').entries()) { + if (invalidTopics.includes(value)) { + topicLabels[index].classList.remove('green'); + topicLabels[index].classList.add('red'); } - } else { - topicPrompts.countPrompt = xhr.responseJSON.message; } } - }).always(() => { - topicForm.form('validate form'); - }); + } }); - topicDropdown.dropdown({ + $(topicDropdown).dropdown({ allowAdditions: true, forceSelection: false, fullTextSearch: 'exact', @@ -99,9 +96,9 @@ export function initRepoTopicBar() { const query = stripTags(this.urlData.query.trim()); let found_query = false; const current_topics = []; - topicDropdown.find('a.label.visible').each((_, el) => { + for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) { current_topics.push(el.getAttribute('data-value')); - }); + } if (res.topics) { let found = false; @@ -143,38 +140,8 @@ export function initRepoTopicBar() { }, onAdd(addedValue, _addedText, $addedChoice) { addedValue = addedValue.toLowerCase().trim(); - $($addedChoice).attr('data-value', addedValue); - $($addedChoice).attr('data-text', addedValue); - } - }); - - $.fn.form.settings.rules.validateTopic = function (_values, regExp) { - const topics = topicDropdown.children('a.ui.label'); - const status = topics.length === 0 || topics.last().attr('data-value').match(regExp); - if (!status) { - topics.last().removeClass('green').addClass('red'); - } - return status && topicDropdown.children('a.ui.label.red').length === 0; - }; - - topicForm.form({ - on: 'change', - inline: true, - fields: { - topics: { - identifier: 'topics', - rules: [ - { - type: 'validateTopic', - value: /^\s*[a-z0-9][-.a-z0-9]{0,35}\s*$/, - prompt: topicPrompts.formatPrompt - }, - { - type: 'maxCount[25]', - prompt: topicPrompts.countPrompt - } - ] - }, - } + $addedChoice[0].setAttribute('data-value', addedValue); + $addedChoice[0].setAttribute('data-text', addedValue); + }, }); } diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js index 7832641687..cef2f49008 100644 --- a/web_src/js/features/repo-issue-content.js +++ b/web_src/js/features/repo-issue-content.js @@ -1,8 +1,10 @@ import $ from 'jquery'; import {svg} from '../svg.js'; import {showErrorToast} from '../modules/toast.js'; +import {GET, POST} from '../modules/fetch.js'; +import {showElem} from '../utils/dom.js'; -const {appSubUrl, csrfToken} = window.config; +const {appSubUrl} = window.config; let i18nTextEdited; let i18nTextOptions; let i18nTextDeleteFromHistory; @@ -15,9 +17,9 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH $dialog = $(` <div class="ui modal content-history-detail-dialog"> ${svg('octicon-x', 16, 'close icon inside')} - <div class="header gt-df gt-ac gt-sb"> + <div class="header tw-flex tw-items-center tw-justify-between"> <div>${itemTitleHtml}</div> - <div class="ui dropdown dialog-header-options gt-mr-5 gt-hidden"> + <div class="ui dropdown dialog-header-options tw-mr-8 tw-hidden"> ${i18nTextOptions} ${svg('octicon-triangle-down', 14, 'dropdown icon')} <div class="menu"> @@ -31,19 +33,27 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH $dialog.find('.dialog-header-options').dropdown({ showOnFocus: false, allowReselection: true, - onChange(_value, _text, $item) { + async onChange(_value, _text, $item) { const optionItem = $item.data('option-item'); if (optionItem === 'delete') { if (window.confirm(i18nTextDeleteFromHistoryConfirm)) { - $.post(`${issueBaseUrl}/content-history/soft-delete?comment_id=${commentId}&history_id=${historyId}`, { - _csrf: csrfToken, - }).done((resp) => { + try { + const params = new URLSearchParams(); + params.append('comment_id', commentId); + params.append('history_id', historyId); + + const response = await POST(`${issueBaseUrl}/content-history/soft-delete?${params.toString()}`); + const resp = await response.json(); + if (resp.ok) { $dialog.modal('hide'); } else { showErrorToast(resp.message); } - }); + } catch (error) { + console.error('Error:', error); + showErrorToast('An error occurred while deleting the history.'); + } } } else { // required by eslint showErrorToast(`unknown option item: ${optionItem}`); @@ -51,22 +61,29 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH }, onHide() { $(this).dropdown('clear', true); - } + }, }); $dialog.modal({ - onShow() { - $.ajax({ - url: `${issueBaseUrl}/content-history/detail?comment_id=${commentId}&history_id=${historyId}`, - data: { - _csrf: csrfToken, - }, - }).done((resp) => { - $dialog.find('.comment-diff-data').removeClass('is-loading').html(resp.diffHtml); + async onShow() { + try { + const params = new URLSearchParams(); + params.append('comment_id', commentId); + params.append('history_id', historyId); + + const url = `${issueBaseUrl}/content-history/detail?${params.toString()}`; + const response = await GET(url); + const resp = await response.json(); + + const commentDiffData = $dialog.find('.comment-diff-data')[0]; + commentDiffData?.classList.remove('is-loading'); + commentDiffData.innerHTML = resp.diffHtml; // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden. if (resp.canSoftDelete) { - $dialog.find('.dialog-header-options').removeClass('gt-hidden'); + showElem($dialog.find('.dialog-header-options')); } - }); + } catch (error) { + console.error('Error:', error); + } }, onHidden() { $dialog.remove(); @@ -103,7 +120,7 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) { }); } -export function initRepoIssueContentHistory() { +export async function initRepoIssueContentHistory() { const issueIndex = $('#issueIndex').val(); if (!issueIndex) return; @@ -114,12 +131,10 @@ export function initRepoIssueContentHistory() { const repoLink = $('#repolink').val(); const issueBaseUrl = `${appSubUrl}/${repoLink}/issues/${issueIndex}`; - $.ajax({ - url: `${issueBaseUrl}/content-history/overview`, - data: { - _csrf: csrfToken, - }, - }).done((resp) => { + try { + const response = await GET(`${issueBaseUrl}/content-history/overview`); + const resp = await response.json(); + i18nTextEdited = resp.i18n.textEdited; i18nTextDeleteFromHistory = resp.i18n.textDeleteFromHistory; i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm; @@ -133,5 +148,7 @@ export function initRepoIssueContentHistory() { const $itemComment = $(`#issuecomment-${commentId}`); showContentHistoryMenu(issueBaseUrl, $itemComment, commentId); } - }); + } catch (error) { + console.error('Error:', error); + } } diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js new file mode 100644 index 0000000000..4c03325c7a --- /dev/null +++ b/web_src/js/features/repo-issue-edit.js @@ -0,0 +1,206 @@ +import $ from 'jquery'; +import {handleReply} from './repo-issue.js'; +import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; +import {createDropzone} from './dropzone.js'; +import {GET, POST} from '../modules/fetch.js'; +import {hideElem, showElem} from '../utils/dom.js'; +import {attachRefIssueContextPopup} from './contextpopup.js'; +import {initCommentContent, initMarkupContent} from '../markup/content.js'; + +const {csrfToken} = window.config; + +async function onEditContent(event) { + event.preventDefault(); + + const segment = this.closest('.header').nextElementSibling; + const editContentZone = segment.querySelector('.edit-content-zone'); + const renderContent = segment.querySelector('.render-content'); + const rawContent = segment.querySelector('.raw-content'); + + let comboMarkdownEditor; + + /** + * @param {HTMLElement} dropzone + */ + const setupDropzone = async (dropzone) => { + if (!dropzone) return null; + + let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event + let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone + const dz = await createDropzone(dropzone, { + url: dropzone.getAttribute('data-upload-url'), + headers: {'X-Csrf-Token': csrfToken}, + maxFiles: dropzone.getAttribute('data-max-file'), + maxFilesize: dropzone.getAttribute('data-max-size'), + acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'), + addRemoveLinks: true, + dictDefaultMessage: dropzone.getAttribute('data-default-message'), + dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'), + dictFileTooBig: dropzone.getAttribute('data-file-too-big'), + dictRemoveFile: dropzone.getAttribute('data-remove-file'), + timeout: 0, + thumbnailMethod: 'contain', + thumbnailWidth: 480, + thumbnailHeight: 480, + init() { + this.on('success', (file, data) => { + file.uuid = data.uuid; + fileUuidDict[file.uuid] = {submitted: false}; + const input = document.createElement('input'); + input.id = data.uuid; + input.name = 'files'; + input.type = 'hidden'; + input.value = data.uuid; + dropzone.querySelector('.files').append(input); + }); + this.on('removedfile', async (file) => { + document.getElementById(file.uuid)?.remove(); + if (disableRemovedfileEvent) return; + if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) { + try { + await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})}); + } catch (error) { + console.error(error); + } + } + }); + this.on('submit', () => { + for (const fileUuid of Object.keys(fileUuidDict)) { + fileUuidDict[fileUuid].submitted = true; + } + }); + this.on('reload', async () => { + try { + const response = await GET(editContentZone.getAttribute('data-attachment-url')); + const data = await response.json(); + // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server + disableRemovedfileEvent = true; + dz.removeAllFiles(true); + dropzone.querySelector('.files').innerHTML = ''; + for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove(); + fileUuidDict = {}; + disableRemovedfileEvent = false; + + for (const attachment of data) { + const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`; + dz.emit('addedfile', attachment); + dz.emit('thumbnail', attachment, imgSrc); + dz.emit('complete', attachment); + fileUuidDict[attachment.uuid] = {submitted: true}; + dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; + const input = document.createElement('input'); + input.id = attachment.uuid; + input.name = 'files'; + input.type = 'hidden'; + input.value = attachment.uuid; + dropzone.querySelector('.files').append(input); + } + if (!dropzone.querySelector('.dz-preview')) { + dropzone.classList.remove('dz-started'); + } + } catch (error) { + console.error(error); + } + }); + }, + }); + dz.emit('reload'); + return dz; + }; + + const cancelAndReset = (e) => { + e.preventDefault(); + showElem(renderContent); + hideElem(editContentZone); + comboMarkdownEditor.attachedDropzoneInst?.emit('reload'); + }; + + const saveAndRefresh = async (e) => { + e.preventDefault(); + showElem(renderContent); + hideElem(editContentZone); + const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst; + try { + const params = new URLSearchParams({ + content: comboMarkdownEditor.value(), + context: editContentZone.getAttribute('data-context'), + }); + for (const fileInput of dropzoneInst?.element.querySelectorAll('.files [name=files]')) params.append('files[]', fileInput.value); + + const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params}); + const data = await response.json(); + if (!data.content) { + renderContent.innerHTML = document.getElementById('no-content').innerHTML; + rawContent.textContent = ''; + } else { + renderContent.innerHTML = data.content; + rawContent.textContent = comboMarkdownEditor.value(); + const refIssues = renderContent.querySelectorAll('p .ref-issue'); + attachRefIssueContextPopup(refIssues); + } + const content = segment; + if (!content.querySelector('.dropzone-attachments')) { + if (data.attachments !== '') { + content.insertAdjacentHTML('beforeend', data.attachments); + } + } else if (data.attachments === '') { + content.querySelector('.dropzone-attachments').remove(); + } else { + content.querySelector('.dropzone-attachments').outerHTML = data.attachments; + } + dropzoneInst?.emit('submit'); + dropzoneInst?.emit('reload'); + initMarkupContent(); + initCommentContent(); + } catch (error) { + console.error(error); + } + }; + + comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); + if (!comboMarkdownEditor) { + editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML; + comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); + comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone')); + editContentZone.querySelector('.cancel.button').addEventListener('click', cancelAndReset); + editContentZone.querySelector('.save.button').addEventListener('click', saveAndRefresh); + } + + // Show write/preview tab and copy raw content as needed + showElem(editContentZone); + hideElem(renderContent); + if (!comboMarkdownEditor.value()) { + comboMarkdownEditor.value(rawContent.textContent); + } + comboMarkdownEditor.focus(); +} + +export function initRepoIssueCommentEdit() { + // Edit issue or comment content + $(document).on('click', '.edit-content', onEditContent); + + // Quote reply + $(document).on('click', '.quote-reply', async function (event) { + event.preventDefault(); + const target = $(this).data('target'); + const quote = $(`#${target}`).text().replace(/\n/g, '\n> '); + const content = `> ${quote}\n\n`; + let editor; + if ($(this).hasClass('quote-reply-diff')) { + const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply'); + editor = await handleReply($replyBtn); + } else { + // for normal issue/comment page + editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor')); + } + if (editor) { + if (editor.value()) { + editor.value(`${editor.value()}\n\n${content}`); + } else { + editor.value(content); + } + editor.focus(); + editor.moveCursorToEnd(); + } + }); +} diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js index ca20cfbe38..92f058c4d2 100644 --- a/web_src/js/features/repo-issue-list.js +++ b/web_src/js/features/repo-issue-list.js @@ -1,39 +1,51 @@ import $ from 'jquery'; import {updateIssuesMeta} from './repo-issue.js'; -import {toggleElem, hideElem} from '../utils/dom.js'; +import {toggleElem, hideElem, isElemHidden} from '../utils/dom.js'; import {htmlEscape} from 'escape-goat'; import {confirmModal} from './comp/ConfirmModal.js'; import {showErrorToast} from '../modules/toast.js'; import {createSortable} from '../modules/sortable.js'; import {DELETE, POST} from '../modules/fetch.js'; +import {parseDom} from '../utils.js'; function initRepoIssueListCheckboxes() { - const $issueSelectAll = $('.issue-checkbox-all'); - const $issueCheckboxes = $('.issue-checkbox'); + const issueSelectAll = document.querySelector('.issue-checkbox-all'); + if (!issueSelectAll) return; // logged out state + const issueCheckboxes = document.querySelectorAll('.issue-checkbox'); const syncIssueSelectionState = () => { - const $checked = $issueCheckboxes.filter(':checked'); - const anyChecked = $checked.length !== 0; - const allChecked = anyChecked && $checked.length === $issueCheckboxes.length; + const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked); + const anyChecked = Boolean(checkedCheckboxes.length); + const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length; if (allChecked) { - $issueSelectAll.prop({'checked': true, 'indeterminate': false}); + issueSelectAll.checked = true; + issueSelectAll.indeterminate = false; } else if (anyChecked) { - $issueSelectAll.prop({'checked': false, 'indeterminate': true}); + issueSelectAll.checked = false; + issueSelectAll.indeterminate = true; } else { - $issueSelectAll.prop({'checked': false, 'indeterminate': false}); + issueSelectAll.checked = false; + issueSelectAll.indeterminate = false; } // if any issue is selected, show the action panel, otherwise show the filter panel toggleElem($('#issue-filters'), !anyChecked); toggleElem($('#issue-actions'), anyChecked); // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel - $('#issue-filters, #issue-actions').filter(':visible').find('.issue-list-toolbar-left').prepend($issueSelectAll); + const panels = document.querySelectorAll('#issue-filters, #issue-actions'); + const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el)); + const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left'); + toolbarLeft.prepend(issueSelectAll); }; - $issueCheckboxes.on('change', syncIssueSelectionState); + for (const el of issueCheckboxes) { + el.addEventListener('change', syncIssueSelectionState); + } - $issueSelectAll.on('change', () => { - $issueCheckboxes.prop('checked', $issueSelectAll.is(':checked')); + issueSelectAll.addEventListener('change', () => { + for (const el of issueCheckboxes) { + el.checked = issueSelectAll.checked; + } syncIssueSelectionState(); }); @@ -69,16 +81,12 @@ function initRepoIssueListCheckboxes() { } } - updateIssuesMeta( - url, - action, - issueIDs, - elementId, - ).then(() => { + try { + await updateIssuesMeta(url, action, issueIDs, elementId); window.location.reload(); - }).catch((reason) => { - showErrorToast(reason.responseJSON.error); - }); + } catch (err) { + showErrorToast(err.responseJSON?.error ?? err.message); + } }); } @@ -86,9 +94,9 @@ function initRepoIssueListAuthorDropdown() { const $searchDropdown = $('.user-remote-search'); if (!$searchDropdown.length) return; - let searchUrl = $searchDropdown.attr('data-search-url'); - const actionJumpUrl = $searchDropdown.attr('data-action-jump-url'); - const selectedUserId = $searchDropdown.attr('data-selected-user-id'); + let searchUrl = $searchDropdown[0].getAttribute('data-search-url'); + const actionJumpUrl = $searchDropdown[0].getAttribute('data-action-jump-url'); + const selectedUserId = $searchDropdown[0].getAttribute('data-selected-user-id'); if (!searchUrl.includes('?')) searchUrl += '?'; $searchDropdown.dropdown('setting', { @@ -101,8 +109,8 @@ function initRepoIssueListAuthorDropdown() { // the content is provided by backend IssuePosters handler const processedResults = []; // to be used by dropdown to generate menu items for (const item of resp.results) { - let html = `<img class="ui avatar gt-vm" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`; - if (item.full_name) html += `<span class="search-fullname gt-ml-3">${htmlEscape(item.full_name)}</span>`; + let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`; + if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`; processedResults.push({value: item.user_id, name: html}); } resp.results = processedResults; @@ -122,20 +130,29 @@ function initRepoIssueListAuthorDropdown() { const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates'); $searchDropdown.dropdown('internal', 'setup', dropdownSetup); dropdownSetup.menu = function (values) { - const $menu = $searchDropdown.find('> .menu'); - $menu.find('> .dynamic-item').remove(); // remove old dynamic items + const menu = $searchDropdown.find('> .menu')[0]; + // remove old dynamic items + for (const el of menu.querySelectorAll(':scope > .dynamic-item')) { + el.remove(); + } const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className')); if (newMenuHtml) { - const $newMenuItems = $(newMenuHtml); - $newMenuItems.addClass('dynamic-item'); - $menu.append('<div class="divider dynamic-item"></div>', ...$newMenuItems); + const newMenuItems = parseDom(newMenuHtml, 'text/html').querySelectorAll('body > div'); + for (const newMenuItem of newMenuItems) { + newMenuItem.classList.add('dynamic-item'); + } + const div = document.createElement('div'); + div.classList.add('divider', 'dynamic-item'); + menu.append(div, ...newMenuItems); } $searchDropdown.dropdown('refresh'); // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function setTimeout(() => { - $menu.find('.item.active, .item.selected').removeClass('active selected'); - $menu.find(`.item[data-value="${selectedUserId}"]`).addClass('selected'); + for (const el of menu.querySelectorAll('.item.active, .item.selected')) { + el.classList.remove('active', 'selected'); + } + menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected'); }, 0); }; } @@ -179,8 +196,6 @@ async function initIssuePinSort() { createSortable(pinDiv, { group: 'shared', - animation: 150, - ghostClass: 'card-ghost', onEnd: pinMoveEnd, }); } diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index 6908e0c912..0d326aae58 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -5,8 +5,10 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.js'; import {setFileFolding} from './file-fold.js'; import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; import {toAbsoluteUrl} from '../utils.js'; +import {initDropzone} from './common-global.js'; +import {POST, GET} from '../modules/fetch.js'; -const {appSubUrl, csrfToken} = window.config; +const {appSubUrl} = window.config; export function initRepoIssueTimeTracking() { $(document).on('click', '.issue-add-time', () => { @@ -39,39 +41,37 @@ export function initRepoIssueTimeTracking() { }); } -function updateDeadline(deadlineString) { - hideElem($('#deadline-err-invalid-date')); - $('#deadline-loader').addClass('loading'); +async function updateDeadline(deadlineString) { + hideElem('#deadline-err-invalid-date'); + document.getElementById('deadline-loader')?.classList.add('is-loading'); let realDeadline = null; if (deadlineString !== '') { const newDate = Date.parse(deadlineString); if (Number.isNaN(newDate)) { - $('#deadline-loader').removeClass('loading'); - showElem($('#deadline-err-invalid-date')); + document.getElementById('deadline-loader')?.classList.remove('is-loading'); + showElem('#deadline-err-invalid-date'); return false; } realDeadline = new Date(newDate); } - $.ajax(`${$('#update-issue-deadline-form').attr('action')}`, { - data: JSON.stringify({ - due_date: realDeadline, - }), - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - type: 'POST', - success() { + try { + const response = await POST(document.getElementById('update-issue-deadline-form').getAttribute('action'), { + data: {due_date: realDeadline}, + }); + + if (response.ok) { window.location.reload(); - }, - error() { - $('#deadline-loader').removeClass('loading'); - showElem($('#deadline-err-invalid-date')); - }, - }); + } else { + throw new Error('Invalid response'); + } + } catch (error) { + console.error(error); + document.getElementById('deadline-loader').classList.remove('is-loading'); + showElem('#deadline-err-invalid-date'); + } } export function initRepoIssueDue() { @@ -87,6 +87,19 @@ export function initRepoIssueDue() { }); } +/** + * @param {HTMLElement} item + */ +function excludeLabel(item) { + const href = item.getAttribute('href'); + const id = item.getAttribute('data-label-id'); + + const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`; + const newStr = 'labels=$1-$2$3&'; + + window.location = href.replace(new RegExp(regStr), newStr); +} + export function initRepoIssueSidebarList() { const repolink = $('#repolink').val(); const repoId = $('#repoId').val(); @@ -123,16 +136,6 @@ export function initRepoIssueSidebarList() { fullTextSearch: true, }); - function excludeLabel(item) { - const href = $(item).attr('href'); - const id = $(item).data('label-id'); - - const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`; - const newStr = 'labels=$1-$2$3&'; - - window.location = href.replace(new RegExp(regStr), newStr); - } - $('.menu a.label-filter-item').each(function () { $(this).on('click', function (e) { if (e.altKey) { @@ -144,9 +147,9 @@ export function initRepoIssueSidebarList() { $('.menu .ui.dropdown.label-filter').on('keydown', (e) => { if (e.altKey && e.keyCode === 13) { - const selectedItems = $('.menu .ui.dropdown.label-filter .menu .item.selected'); - if (selectedItems.length > 0) { - excludeLabel($(selectedItems[0])); + const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected'); + if (selectedItem) { + excludeLabel(selectedItem); } } }); @@ -155,39 +158,55 @@ export function initRepoIssueSidebarList() { export function initRepoIssueCommentDelete() { // Delete comment - $(document).on('click', '.delete-comment', function () { - const $this = $(this); - if (window.confirm($this.data('locale'))) { - $.post($this.data('url'), { - _csrf: csrfToken, - }).done(() => { - const $conversationHolder = $this.closest('.conversation-holder'); + document.addEventListener('click', async (e) => { + if (!e.target.matches('.delete-comment')) return; + e.preventDefault(); + + const deleteButton = e.target; + if (window.confirm(deleteButton.getAttribute('data-locale'))) { + try { + const response = await POST(deleteButton.getAttribute('data-url')); + if (!response.ok) throw new Error('Failed to delete comment'); + + const conversationHolder = deleteButton.closest('.conversation-holder'); + const parentTimelineItem = deleteButton.closest('.timeline-item'); + const parentTimelineGroup = deleteButton.closest('.timeline-item-group'); // Check if this was a pending comment. - if ($conversationHolder.find('.pending-label').length) { - const $counter = $('#review-box .review-comments-counter'); - let num = parseInt($counter.attr('data-pending-comment-number')) - 1 || 0; + if (conversationHolder?.querySelector('.pending-label')) { + const counter = document.querySelector('#review-box .review-comments-counter'); + let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0; num = Math.max(num, 0); - $counter.attr('data-pending-comment-number', num); - $counter.text(num); + counter.setAttribute('data-pending-comment-number', num); + counter.textContent = String(num); } - $(`#${$this.data('comment-id')}`).remove(); - if ($conversationHolder.length && !$conversationHolder.find('.comment').length) { - const path = $conversationHolder.data('path'); - const side = $conversationHolder.data('side'); - const idx = $conversationHolder.data('idx'); - const lineType = $conversationHolder.closest('tr').data('line-type'); + document.getElementById(deleteButton.getAttribute('data-comment-id'))?.remove(); + + if (conversationHolder && !conversationHolder.querySelector('.comment')) { + const path = conversationHolder.getAttribute('data-path'); + const side = conversationHolder.getAttribute('data-side'); + const idx = conversationHolder.getAttribute('data-idx'); + const lineType = conversationHolder.closest('tr').getAttribute('data-line-type'); + if (lineType === 'same') { - $(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).removeClass('gt-invisible'); + document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible'); } else { - $(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).removeClass('gt-invisible'); + document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible'); } - $conversationHolder.remove(); + + conversationHolder.remove(); } - }); + + // Check if there is no review content, move the time avatar upward to avoid overlapping the content below. + if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) { + const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar'); + timelineAvatar?.classList.remove('timeline-avatar-offset'); + } + } catch (error) { + console.error(error); + } } - return false; }); } @@ -211,46 +230,62 @@ export function initRepoIssueDependencyDelete() { export function initRepoIssueCodeCommentCancel() { // Cancel inline code comment - $(document).on('click', '.cancel-code-comment', (e) => { - const form = $(e.currentTarget).closest('form'); - if (form.length > 0 && form.hasClass('comment-form')) { - form.addClass('gt-hidden'); - showElem(form.closest('.comment-code-cloud').find('button.comment-form-reply')); + document.addEventListener('click', (e) => { + if (!e.target.matches('.cancel-code-comment')) return; + + const form = e.target.closest('form'); + if (form?.classList.contains('comment-form')) { + hideElem(form); + showElem(form.closest('.comment-code-cloud')?.querySelectorAll('button.comment-form-reply')); } else { - form.closest('.comment-code-cloud').remove(); + form.closest('.comment-code-cloud')?.remove(); } }); } export function initRepoPullRequestUpdate() { // Pull Request update button - const $pullUpdateButton = $('.update-button > button'); - $pullUpdateButton.on('click', function (e) { + const pullUpdateButton = document.querySelector('.update-button > button'); + if (!pullUpdateButton) return; + + pullUpdateButton.addEventListener('click', async function (e) { e.preventDefault(); - const $this = $(this); - const redirect = $this.data('redirect'); - $this.addClass('loading'); - $.post($this.data('do'), { - _csrf: csrfToken - }).done((data) => { - if (data.redirect) { - window.location.href = data.redirect; - } else if (redirect) { - window.location.href = redirect; - } else { - window.location.reload(); - } - }); + const redirect = this.getAttribute('data-redirect'); + this.classList.add('is-loading'); + let response; + try { + response = await POST(this.getAttribute('data-do')); + } catch (error) { + console.error(error); + } finally { + this.classList.remove('is-loading'); + } + let data; + try { + data = await response?.json(); // the response is probably not a JSON + } catch (error) { + console.error(error); + } + if (data?.redirect) { + window.location.href = data.redirect; + } else if (redirect) { + window.location.href = redirect; + } else { + window.location.reload(); + } }); $('.update-button > .dropdown').dropdown({ onChange(_text, _value, $choice) { - const $url = $choice.data('do'); - if ($url) { - $pullUpdateButton.find('.button-text').text($choice.text()); - $pullUpdateButton.data('do', $url); + const url = $choice[0].getAttribute('data-do'); + if (url) { + const buttonText = pullUpdateButton.querySelector('.button-text'); + if (buttonText) { + buttonText.textContent = $choice.text(); + } + pullUpdateButton.setAttribute('data-do', url); } - } + }, }); } @@ -261,26 +296,26 @@ export function initRepoPullRequestMergeInstruction() { } export function initRepoPullRequestAllowMaintainerEdit() { - const $checkbox = $('#allow-edits-from-maintainers'); - if (!$checkbox.length) return; + const wrapper = document.getElementById('allow-edits-from-maintainers'); + if (!wrapper) return; - const promptError = $checkbox.attr('data-prompt-error'); - $checkbox.checkbox({ - 'onChange': () => { - const checked = $checkbox.checkbox('is checked'); - let url = $checkbox.attr('data-url'); - url += '/set_allow_maintainer_edit'; - $checkbox.checkbox('set disabled'); - $.ajax({url, type: 'POST', - data: {_csrf: csrfToken, allow_maintainer_edit: checked}, - error: () => { - showTemporaryTooltip($checkbox[0], promptError); - }, - complete: () => { - $checkbox.checkbox('set enabled'); - }, - }); - }, + wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => { + const checked = e.target.checked; + const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`; + wrapper.classList.add('is-loading'); + e.target.disabled = true; + try { + const response = await POST(url, {data: {allow_maintainer_edit: checked}}); + if (!response.ok) { + throw new Error('Failed to update maintainer edit permission'); + } + } catch (error) { + console.error(error); + showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error')); + } finally { + wrapper.classList.remove('is-loading'); + e.target.disabled = false; + } }); } @@ -294,7 +329,7 @@ export function initRepoIssueReferenceRepositorySearch() { $.each(response.data, (_r, repo) => { filteredResponse.results.push({ name: htmlEscape(repo.repository.full_name), - value: repo.repository.full_name + value: repo.repository.full_name, }); }); return filteredResponse; @@ -303,9 +338,11 @@ export function initRepoIssueReferenceRepositorySearch() { }, onChange(_value, _text, $choice) { const $form = $choice.closest('form'); - $form.attr('action', `${appSubUrl}/${_text}/issues/new`); + if (!$form.length) return; + + $form[0].setAttribute('action', `${appSubUrl}/${_text}/issues/new`); }, - fullTextSearch: true + fullTextSearch: true, }); } @@ -328,48 +365,41 @@ export function initRepoIssueWipTitle() { }); } -export async function updateIssuesMeta(url, action, issueIds, elementId) { - return $.ajax({ - type: 'POST', - url, - data: { - _csrf: csrfToken, - action, - issue_ids: issueIds, - id: elementId, - }, - }); +export async function updateIssuesMeta(url, action, issue_ids, id) { + try { + const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); + if (!response.ok) { + throw new Error('Failed to update issues meta'); + } + } catch (error) { + console.error(error); + } } export function initRepoIssueComments() { - if ($('.repository.view.issue .timeline').length === 0) return; + if (!$('.repository.view.issue .timeline').length) return; - $('.re-request-review').on('click', function (e) { + $('.re-request-review').on('click', async function (e) { e.preventDefault(); - const url = $(this).data('update-url'); - const issueId = $(this).data('issue-id'); - const id = $(this).data('id'); - const isChecked = $(this).hasClass('checked'); + const url = this.getAttribute('data-update-url'); + const issueId = this.getAttribute('data-issue-id'); + const id = this.getAttribute('data-id'); + const isChecked = this.classList.contains('checked'); - updateIssuesMeta( - url, - isChecked ? 'detach' : 'attach', - issueId, - id, - ).then(() => window.location.reload()); + await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id); + window.location.reload(); }); - $(document).on('click', (event) => { - const urlTarget = $(':target'); - if (urlTarget.length === 0) return; + document.addEventListener('click', (e) => { + const urlTarget = document.querySelector(':target'); + if (!urlTarget) return; - const urlTargetId = urlTarget.attr('id'); + const urlTargetId = urlTarget.id; if (!urlTargetId) return; + if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return; - const $target = $(event.target); - - if ($target.closest(`#${urlTargetId}`).length === 0) { + if (!e.target.closest(`#${urlTargetId}`)) { const scrollPosition = $(window).scrollTop(); window.location.hash = ''; $(window).scrollTop(scrollPosition); @@ -380,13 +410,18 @@ export function initRepoIssueComments() { export async function handleReply($el) { hideElem($el); - const form = $el.closest('.comment-code-cloud').find('.comment-form'); - form.removeClass('gt-hidden'); + const $form = $el.closest('.comment-code-cloud').find('.comment-form'); + showElem($form); - const $textarea = form.find('textarea'); + const $textarea = $form.find('textarea'); let editor = getComboMarkdownEditor($textarea); if (!editor) { - editor = await initComboMarkdownEditor(form.find('.combo-markdown-editor')); + // FIXME: the initialization of the dropzone is not consistent. + // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized. + // When the form is submitted and partially reload, none of them is initialized. + const dropzone = $form.find('.dropzone')[0]; + if (!dropzone.dropzone) initDropzone(dropzone); + editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor')); } editor.focus(); return editor; @@ -398,31 +433,34 @@ export function initRepoPullRequestReview() { if (window.history.scrollRestoration !== 'manual') { window.history.scrollRestoration = 'manual'; } - const commentDiv = $(window.location.hash); + const commentDiv = document.querySelector(window.location.hash); if (commentDiv) { // get the name of the parent id - const groupID = commentDiv.closest('div[id^="code-comments-"]').attr('id'); + const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id'); if (groupID && groupID.startsWith('code-comments-')) { const id = groupID.slice(14); const ancestorDiffBox = commentDiv.closest('.diff-file-box'); // on pages like conversation, there is no diff header - const diffHeader = ancestorDiffBox.find('.diff-file-header'); + const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header'); + // offset is for scrolling let offset = 30; - if (diffHeader[0]) { - offset += $('.diff-detail-box').outerHeight() + diffHeader.outerHeight(); + if (diffHeader) { + offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight(); } - $(`#show-outdated-${id}`).addClass('gt-hidden'); - $(`#code-comments-${id}`).removeClass('gt-hidden'); - $(`#code-preview-${id}`).removeClass('gt-hidden'); - $(`#hide-outdated-${id}`).removeClass('gt-hidden'); + + document.getElementById(`show-outdated-${id}`).classList.add('tw-hidden'); + document.getElementById(`code-comments-${id}`).classList.remove('tw-hidden'); + document.getElementById(`code-preview-${id}`).classList.remove('tw-hidden'); + document.getElementById(`hide-outdated-${id}`).classList.remove('tw-hidden'); // if the comment box is folded, expand it - if (ancestorDiffBox.attr('data-folded') && ancestorDiffBox.attr('data-folded') === 'true') { - setFileFolding(ancestorDiffBox[0], ancestorDiffBox.find('.fold-file')[0], false); + if (ancestorDiffBox.getAttribute('data-folded') === 'true') { + setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false); } + window.scrollTo({ - top: commentDiv.offset().top - offset, - behavior: 'instant' + top: $(commentDiv).offset().top - offset, + behavior: 'instant', }); } } @@ -430,20 +468,20 @@ export function initRepoPullRequestReview() { $(document).on('click', '.show-outdated', function (e) { e.preventDefault(); - const id = $(this).data('comment'); - $(this).addClass('gt-hidden'); - $(`#code-comments-${id}`).removeClass('gt-hidden'); - $(`#code-preview-${id}`).removeClass('gt-hidden'); - $(`#hide-outdated-${id}`).removeClass('gt-hidden'); + const id = this.getAttribute('data-comment'); + hideElem(this); + showElem(`#code-comments-${id}`); + showElem(`#code-preview-${id}`); + showElem(`#hide-outdated-${id}`); }); $(document).on('click', '.hide-outdated', function (e) { e.preventDefault(); - const id = $(this).data('comment'); - $(this).addClass('gt-hidden'); - $(`#code-comments-${id}`).addClass('gt-hidden'); - $(`#code-preview-${id}`).addClass('gt-hidden'); - $(`#show-outdated-${id}`).removeClass('gt-hidden'); + const id = this.getAttribute('data-comment'); + hideElem(this); + hideElem(`#code-comments-${id}`); + hideElem(`#code-preview-${id}`); + showElem(`#show-outdated-${id}`); }); $(document).on('click', 'button.comment-form-reply', async function (e) { @@ -457,9 +495,7 @@ export function initRepoPullRequestReview() { } // The following part is only for diff views - if ($('.repository.pull.diff').length === 0) { - return; - } + if (!$('.repository.pull.diff').length) return; const $reviewBtn = $('.js-btn-review'); const $panel = $reviewBtn.parent().find('.review-box-panel'); @@ -482,19 +518,20 @@ export function initRepoPullRequestReview() { } $(document).on('click', '.add-code-comment', async function (e) { - if ($(e.target).hasClass('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745 + if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745 e.preventDefault(); - const isSplit = $(this).closest('.code-diff').hasClass('code-diff-split'); - const side = $(this).data('side'); - const idx = $(this).data('idx'); - const path = $(this).closest('[data-path]').data('path'); - const tr = $(this).closest('tr'); - const lineType = tr.data('line-type'); + const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split'); + const side = this.getAttribute('data-side'); + const idx = this.getAttribute('data-idx'); + const path = this.closest('[data-path]')?.getAttribute('data-path'); + const tr = this.closest('tr'); + const lineType = tr.getAttribute('data-line-type'); - let ntr = tr.next(); - if (!ntr.hasClass('add-comment')) { - ntr = $(` + const ntr = tr.nextElementSibling; + let $ntr = $(ntr); + if (!ntr?.classList.contains('add-comment')) { + $ntr = $(` <tr class="add-comment" data-line-type="${lineType}"> ${isSplit ? ` <td class="add-comment-left" colspan="4"></td> @@ -503,20 +540,26 @@ export function initRepoPullRequestReview() { <td class="add-comment-left add-comment-right" colspan="5"></td> `} </tr>`); - tr.after(ntr); + $(tr).after($ntr); } - const td = ntr.find(`.add-comment-${side}`); - const commentCloud = td.find('.comment-code-cloud'); - if (commentCloud.length === 0 && !ntr.find('button[name="pending_review"]').length) { - const html = await $.get($(this).closest('[data-new-comment-url]').attr('data-new-comment-url')); - td.html(html); - td.find("input[name='line']").val(idx); - td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed'); - td.find("input[name='path']").val(path); + const $td = $ntr.find(`.add-comment-${side}`); + const $commentCloud = $td.find('.comment-code-cloud'); + if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) { + try { + const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url')); + const html = await response.text(); + $td.html(html); + $td.find("input[name='line']").val(idx); + $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed'); + $td.find("input[name='path']").val(path); - const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor')); - editor.focus(); + initDropzone($td.find('.dropzone')[0]); + const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor')); + editor.focus(); + } catch (error) { + console.error(error); + } } }); } @@ -544,14 +587,38 @@ export function initRepoIssueWipToggle() { const title = toggleWip.getAttribute('data-title'); const wipPrefix = toggleWip.getAttribute('data-wip-prefix'); const updateUrl = toggleWip.getAttribute('data-update-url'); - await $.post(updateUrl, { - _csrf: csrfToken, - title: title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`, - }); - window.location.reload(); + + try { + const params = new URLSearchParams(); + params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`); + + const response = await POST(updateUrl, {data: params}); + if (!response.ok) { + throw new Error('Failed to toggle WIP status'); + } + window.location.reload(); + } catch (error) { + console.error(error); + } }); } +async function pullrequest_targetbranch_change(update_url) { + const targetBranch = $('#pull-target-branch').data('branch'); + const $branchTarget = $('#branch_target'); + if (targetBranch === $branchTarget.text()) { + window.location.reload(); + return false; + } + try { + await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})}); + } catch (error) { + console.error(error); + } finally { + window.location.reload(); + } +} + export function initRepoIssueTitleEdit() { // Edit issue title const $issueTitle = $('#issue-title'); @@ -559,13 +626,13 @@ export function initRepoIssueTitleEdit() { const editTitleToggle = function () { toggleElem($issueTitle); - toggleElem($('.not-in-edit')); - toggleElem($('#edit-title-input')); - toggleElem($('#pull-desc')); - toggleElem($('#pull-desc-edit')); - toggleElem($('.in-edit')); - toggleElem($('.new-issue-button')); - $('#issue-title-wrapper').toggleClass('edit-active'); + toggleElem('.not-in-edit'); + toggleElem('#edit-title-input'); + toggleElem('#pull-desc'); + toggleElem('#pull-desc-edit'); + toggleElem('.in-edit'); + toggleElem('.new-issue-button'); + document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active'); $editInput[0].focus(); $editInput[0].select(); return false; @@ -573,39 +640,27 @@ export function initRepoIssueTitleEdit() { $('#edit-title').on('click', editTitleToggle); $('#cancel-edit-title').on('click', editTitleToggle); - $('#save-edit-title').on('click', editTitleToggle).on('click', function () { - const pullrequest_targetbranch_change = function (update_url) { - const targetBranch = $('#pull-target-branch').data('branch'); - const $branchTarget = $('#branch_target'); - if (targetBranch === $branchTarget.text()) { - window.location.reload(); - return false; - } - $.post(update_url, { - _csrf: csrfToken, - target_branch: targetBranch - }).always(() => { - window.location.reload(); - }); - }; - - const pullrequest_target_update_url = $(this).attr('data-target-update-url'); - if ($editInput.val().length === 0 || $editInput.val() === $issueTitle.text()) { + $('#save-edit-title').on('click', editTitleToggle).on('click', async function () { + const pullrequest_target_update_url = this.getAttribute('data-target-update-url'); + if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) { $editInput.val($issueTitle.text()); - pullrequest_targetbranch_change(pullrequest_target_update_url); + await pullrequest_targetbranch_change(pullrequest_target_update_url); } else { - $.post($(this).attr('data-update-url'), { - _csrf: csrfToken, - title: $editInput.val() - }, (data) => { + try { + const params = new URLSearchParams(); + params.append('title', $editInput.val()); + const response = await POST(this.getAttribute('data-update-url'), {data: params}); + const data = await response.json(); $editInput.val(data.title); $issueTitle.text(data.title); if (pullrequest_target_update_url) { - pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window + await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window } else { window.location.reload(); } - }); + } catch (error) { + console.error(error); + } } return false; }); @@ -613,18 +668,18 @@ export function initRepoIssueTitleEdit() { export function initRepoIssueBranchSelect() { const changeBranchSelect = function () { - const selectionTextField = $('#pull-target-branch'); + const $selectionTextField = $('#pull-target-branch'); - const baseName = selectionTextField.data('basename'); + const baseName = $selectionTextField.data('basename'); const branchNameNew = $(this).data('branch'); - const branchNameOld = selectionTextField.data('branch'); + const branchNameOld = $selectionTextField.data('branch'); // Replace branch name to keep translation from HTML template - selectionTextField.html(selectionTextField.html().replace( + $selectionTextField.html($selectionTextField.html().replace( `${baseName}:${branchNameOld}`, - `${baseName}:${branchNameNew}` + `${baseName}:${branchNameNew}`, )); - selectionTextField.data('branch', branchNameNew); // update branch name in setting + $selectionTextField.data('branch', branchNameNew); // update branch name in setting }; $('#branch-select > .item').on('click', changeBranchSelect); } @@ -634,10 +689,11 @@ export function initSingleCommentEditor($commentForm) { // * normal new issue/pr page, no status-button // * issue/pr view page, with comment form, has status-button const opts = {}; - const $statusButton = $('#status-button'); - if ($statusButton.length) { + const statusButton = document.getElementById('status-button'); + if (statusButton) { opts.onContentChanged = (editor) => { - $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status')); + const statusText = statusButton.getAttribute(editor.value().trim() ? 'data-status-and-comment' : 'data-status'); + statusButton.textContent = statusText; }; } initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts); @@ -656,7 +712,7 @@ export function initIssueTemplateCommentEditors($commentForm) { const editor = await initComboMarkdownEditor($markdownEditor, { onContentChanged: (editor) => { $formField.val(editor.value()); - } + }, }); $formField.on('focus', async () => { diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 08fe21190a..e83de27e4c 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -3,7 +3,7 @@ import { initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete, initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue, initRepoIssueTitleEdit, initRepoIssueWipToggle, - initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor, + initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor, } from './repo-issue.js'; import {initUnicodeEscapeButton} from './repo-unicode-escape.js'; import {svg} from '../svg.js'; @@ -15,22 +15,18 @@ import { import {initCitationFileCopyContent} from './citation.js'; import {initCompLabelEdit} from './comp/LabelEdit.js'; import {initRepoDiffConversationNav} from './repo-diff.js'; -import {createDropzone} from './dropzone.js'; -import {initCommentContent, initMarkupContent} from '../markup/content.js'; import {initCompReactionSelector} from './comp/ReactionSelector.js'; import {initRepoSettingBranches} from './repo-settings.js'; import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js'; import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js'; import {hideElem, showElem} from '../utils/dom.js'; -import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; -import {attachRefIssueContextPopup} from './contextpopup.js'; - -const {csrfToken} = window.config; +import {POST} from '../modules/fetch.js'; +import {initRepoIssueCommentEdit} from './repo-issue-edit.js'; // if there are draft comments, confirm before reloading, to avoid losing comments function reloadConfirmDraftComment() { const commentTextareas = [ - document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'), + document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'), document.querySelector('#comment-form textarea'), ]; for (const textarea of commentTextareas) { @@ -49,9 +45,7 @@ function reloadConfirmDraftComment() { export function initRepoCommentForm() { const $commentForm = $('.comment.form'); - if ($commentForm.length === 0) { - return; - } + if (!$commentForm.length) return; if ($commentForm.find('.field.combo-editor-dropzone').length) { // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form @@ -65,7 +59,7 @@ export function initRepoCommentForm() { const $selectBranch = $('.ui.select-branch'); const $branchMenu = $selectBranch.find('.reference-list-menu'); const $isNewIssue = $branchMenu.hasClass('new-issue'); - $branchMenu.find('.item:not(.no-select)').on('click', function () { + $branchMenu.find('.item:not(.no-select)').on('click', async function () { const selectedValue = $(this).data('id'); const editMode = $('#editing_mode').val(); $($(this).data('id-selector')).val(selectedValue); @@ -75,8 +69,15 @@ export function initRepoCommentForm() { } if (editMode === 'true') { - const form = $('#update_issueref_form'); - $.post(form.attr('action'), {_csrf: csrfToken, ref: selectedValue}, () => window.location.reload()); + const form = document.getElementById('update_issueref_form'); + const params = new URLSearchParams(); + params.append('ref', selectedValue); + try { + await POST(form.getAttribute('action'), {data: params}); + window.location.reload(); + } catch (error) { + console.error(error); + } } else if (editMode === '') { $selectBranch.find('.ui .branch-name').text(selectedValue); } @@ -131,26 +132,26 @@ export function initRepoCommentForm() { hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var - const clickedItem = $(this); - const scope = $(this).attr('data-scope'); + const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment + const scope = this.getAttribute('data-scope'); $(this).parent().find('.item').each(function () { if (scope) { // Enable only clicked item for scoped labels - if ($(this).attr('data-scope') !== scope) { + if (this.getAttribute('data-scope') !== scope) { return true; } - if (!$(this).is(clickedItem) && !$(this).hasClass('checked')) { + if (this !== clickedItem && !$(this).hasClass('checked')) { return true; } - } else if (!$(this).is(clickedItem)) { + } else if (this !== clickedItem) { // Toggle for other labels return true; } if ($(this).hasClass('checked')) { $(this).removeClass('checked'); - $(this).find('.octicon-check').addClass('gt-invisible'); + $(this).find('.octicon-check').addClass('tw-invisible'); if (hasUpdateAction) { if (!($(this).data('id') in items)) { items[$(this).data('id')] = { @@ -164,7 +165,7 @@ export function initRepoCommentForm() { } } else { $(this).addClass('checked'); - $(this).find('.octicon-check').removeClass('gt-invisible'); + $(this).find('.octicon-check').removeClass('tw-invisible'); if (hasUpdateAction) { if (!($(this).data('id') in items)) { items[$(this).data('id')] = { @@ -189,15 +190,15 @@ export function initRepoCommentForm() { $(this).parent().find('.item').each(function () { if ($(this).hasClass('checked')) { listIds.push($(this).data('id')); - $($(this).data('id-selector')).removeClass('gt-hidden'); + $($(this).data('id-selector')).removeClass('tw-hidden'); } else { - $($(this).data('id-selector')).addClass('gt-hidden'); + $($(this).data('id-selector')).addClass('tw-hidden'); } }); - if (listIds.length === 0) { - $noSelect.removeClass('gt-hidden'); + if (!listIds.length) { + $noSelect.removeClass('tw-hidden'); } else { - $noSelect.addClass('gt-hidden'); + $noSelect.addClass('tw-hidden'); } $($(this).parent().data('id')).val(listIds.join(',')); return false; @@ -205,17 +206,20 @@ export function initRepoCommentForm() { $listMenu.find('.no-select.item').on('click', function (e) { e.preventDefault(); if (hasUpdateAction) { - updateIssuesMeta( - $listMenu.data('update-url'), - 'clear', - $listMenu.data('issue-id'), - '', - ).then(reloadConfirmDraftComment); + (async () => { + await updateIssuesMeta( + $listMenu.data('update-url'), + 'clear', + $listMenu.data('issue-id'), + '', + ); + reloadConfirmDraftComment(); + })(); } $(this).parent().find('.item').each(function () { $(this).removeClass('checked'); - $(this).find('.octicon-check').addClass('gt-invisible'); + $(this).find('.octicon-check').addClass('tw-invisible'); }); if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') { @@ -223,9 +227,9 @@ export function initRepoCommentForm() { } $list.find('.item').each(function () { - $(this).addClass('gt-hidden'); + $(this).addClass('tw-hidden'); }); - $noSelect.removeClass('gt-hidden'); + $noSelect.removeClass('tw-hidden'); $($(this).parent().data('id')).val(''); }); } @@ -248,21 +252,24 @@ export function initRepoCommentForm() { $(this).addClass('selected active'); if (hasUpdateAction) { - updateIssuesMeta( - $menu.data('update-url'), - '', - $menu.data('issue-id'), - $(this).data('id'), - ).then(reloadConfirmDraftComment); + (async () => { + await updateIssuesMeta( + $menu.data('update-url'), + '', + $menu.data('issue-id'), + $(this).data('id'), + ); + reloadConfirmDraftComment(); + })(); } let icon = ''; if (input_id === '#milestone_id') { - icon = svg('octicon-milestone', 18, 'gt-mr-3'); + icon = svg('octicon-milestone', 18, 'tw-mr-2'); } else if (input_id === '#project_id') { - icon = svg('octicon-project', 18, 'gt-mr-3'); + icon = svg('octicon-project', 18, 'tw-mr-2'); } else if (input_id === '#assignee_id') { - icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`; + icon = `<img class="ui avatar image tw-mr-2" alt="avatar" src=${$(this).data('avatar')}>`; } $list.find('.selected').html(` @@ -272,7 +279,7 @@ export function initRepoCommentForm() { </a> `); - $(`.ui${select_id}.list .no-select`).addClass('gt-hidden'); + $(`.ui${select_id}.list .no-select`).addClass('tw-hidden'); $(input_id).val($(this).data('id')); }); $menu.find('.no-select.item').on('click', function () { @@ -281,16 +288,19 @@ export function initRepoCommentForm() { }); if (hasUpdateAction) { - updateIssuesMeta( - $menu.data('update-url'), - '', - $menu.data('issue-id'), - $(this).data('id'), - ).then(reloadConfirmDraftComment); + (async () => { + await updateIssuesMeta( + $menu.data('update-url'), + '', + $menu.data('issue-id'), + $(this).data('id'), + ); + reloadConfirmDraftComment(); + })(); } $list.find('.selected').html(''); - $list.find('.no-select').removeClass('gt-hidden'); + $list.find('.no-select').removeClass('tw-hidden'); $(input_id).val(''); }); } @@ -301,167 +311,8 @@ export function initRepoCommentForm() { selectItem('.select-assignee', '#assignee_id'); } -async function onEditContent(event) { - event.preventDefault(); - - const $segment = $(this).closest('.header').next(); - const $editContentZone = $segment.find('.edit-content-zone'); - const $renderContent = $segment.find('.render-content'); - const $rawContent = $segment.find('.raw-content'); - - let comboMarkdownEditor; - - const setupDropzone = async ($dropzone) => { - if ($dropzone.length === 0) return null; - - let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event - let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone - const dz = await createDropzone($dropzone[0], { - url: $dropzone.attr('data-upload-url'), - headers: {'X-Csrf-Token': csrfToken}, - maxFiles: $dropzone.attr('data-max-file'), - maxFilesize: $dropzone.attr('data-max-size'), - acceptedFiles: (['*/*', ''].includes($dropzone.attr('data-accepts'))) ? null : $dropzone.attr('data-accepts'), - addRemoveLinks: true, - dictDefaultMessage: $dropzone.attr('data-default-message'), - dictInvalidFileType: $dropzone.attr('data-invalid-input-type'), - dictFileTooBig: $dropzone.attr('data-file-too-big'), - dictRemoveFile: $dropzone.attr('data-remove-file'), - timeout: 0, - thumbnailMethod: 'contain', - thumbnailWidth: 480, - thumbnailHeight: 480, - init() { - this.on('success', (file, data) => { - file.uuid = data.uuid; - fileUuidDict[file.uuid] = {submitted: false}; - const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); - $dropzone.find('.files').append(input); - }); - this.on('removedfile', (file) => { - if (disableRemovedfileEvent) return; - $(`#${file.uuid}`).remove(); - if ($dropzone.attr('data-remove-url') && !fileUuidDict[file.uuid].submitted) { - $.post($dropzone.attr('data-remove-url'), { - file: file.uuid, - _csrf: csrfToken, - }); - } - }); - this.on('submit', () => { - $.each(fileUuidDict, (fileUuid) => { - fileUuidDict[fileUuid].submitted = true; - }); - }); - this.on('reload', () => { - $.getJSON($editContentZone.attr('data-attachment-url'), (data) => { - // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server - disableRemovedfileEvent = true; - dz.removeAllFiles(true); - $dropzone.find('.files').empty(); - fileUuidDict = {}; - disableRemovedfileEvent = false; - - for (const attachment of data) { - const imgSrc = `${$dropzone.attr('data-link-url')}/${attachment.uuid}`; - dz.emit('addedfile', attachment); - dz.emit('thumbnail', attachment, imgSrc); - dz.emit('complete', attachment); - dz.files.push(attachment); - fileUuidDict[attachment.uuid] = {submitted: true}; - $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%'); - const input = $(`<input id="${attachment.uuid}" name="files" type="hidden">`).val(attachment.uuid); - $dropzone.find('.files').append(input); - } - }); - }); - }, - }); - dz.emit('reload'); - return dz; - }; - - const cancelAndReset = (dz) => { - showElem($renderContent); - hideElem($editContentZone); - if (dz) { - dz.emit('reload'); - } - }; - - const saveAndRefresh = (dz, $dropzone) => { - showElem($renderContent); - hideElem($editContentZone); - const $attachments = $dropzone.find('.files').find('[name=files]').map(function () { - return $(this).val(); - }).get(); - $.post($editContentZone.attr('data-update-url'), { - _csrf: csrfToken, - content: comboMarkdownEditor.value(), - context: $editContentZone.attr('data-context'), - files: $attachments, - }, (data) => { - if (!data.content) { - $renderContent.html($('#no-content').html()); - $rawContent.text(''); - } else { - $renderContent.html(data.content); - $rawContent.text(comboMarkdownEditor.value()); - - const refIssues = $renderContent.find('p .ref-issue'); - attachRefIssueContextPopup(refIssues); - } - const $content = $segment; - if (!$content.find('.dropzone-attachments').length) { - if (data.attachments !== '') { - $content.append(`<div class="dropzone-attachments"></div>`); - $content.find('.dropzone-attachments').replaceWith(data.attachments); - } - } else if (data.attachments === '') { - $content.find('.dropzone-attachments').remove(); - } else { - $content.find('.dropzone-attachments').replaceWith(data.attachments); - } - if (dz) { - dz.emit('submit'); - dz.emit('reload'); - } - initMarkupContent(); - initCommentContent(); - }); - }; - - if (!$editContentZone.html()) { - $editContentZone.html($('#issue-comment-editor-template').html()); - comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor')); - - const $dropzone = $editContentZone.find('.dropzone'); - const dz = await setupDropzone($dropzone); - $editContentZone.find('.cancel.button').on('click', (e) => { - e.preventDefault(); - cancelAndReset(dz); - }); - $editContentZone.find('.save.button').on('click', (e) => { - e.preventDefault(); - saveAndRefresh(dz, $dropzone); - }); - } else { - comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor')); - } - - // Show write/preview tab and copy raw content as needed - showElem($editContentZone); - hideElem($renderContent); - if (!comboMarkdownEditor.value()) { - comboMarkdownEditor.value($rawContent.text()); - } - comboMarkdownEditor.focus(); -} - export function initRepository() { - if ($('.page-content.repository').length === 0) { - return; - } + if (!$('.page-content.repository').length) return; initRepoBranchTagSelector('.js-branch-tag-selector'); @@ -510,7 +361,7 @@ export function initRepository() { const gitignores = $('input[name="gitignores"]').val(); const license = $('input[name="license"]').val(); if (gitignores || license) { - $('input[name="auto_init"]').prop('checked', true); + document.querySelector('input[name="auto_init"]').checked = true; } }); } @@ -563,33 +414,3 @@ export function initRepository() { initUnicodeEscapeButton(); } - -function initRepoIssueCommentEdit() { - // Edit issue or comment content - $(document).on('click', '.edit-content', onEditContent); - - // Quote reply - $(document).on('click', '.quote-reply', async function (event) { - event.preventDefault(); - const target = $(this).data('target'); - const quote = $(`#${target}`).text().replace(/\n/g, '\n> '); - const content = `> ${quote}\n\n`; - let editor; - if ($(this).hasClass('quote-reply-diff')) { - const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply'); - editor = await handleReply($replyBtn); - } else { - // for normal issue/comment page - editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor')); - } - if (editor) { - if (editor.value()) { - editor.value(`${editor.value()}\n\n${content}`); - } else { - editor.value(content); - } - editor.focus(); - editor.moveCursorToEnd(); - } - }); -} diff --git a/web_src/js/features/repo-migrate.js b/web_src/js/features/repo-migrate.js index cae28fdd1b..490e7df0e4 100644 --- a/web_src/js/features/repo-migrate.js +++ b/web_src/js/features/repo-migrate.js @@ -1,18 +1,17 @@ -import $ from 'jquery'; import {hideElem, showElem} from '../utils/dom.js'; import {GET, POST} from '../modules/fetch.js'; const {appSubUrl} = window.config; export function initRepoMigrationStatusChecker() { - const $repoMigrating = $('#repo_migrating'); - if (!$repoMigrating.length) return; + const repoMigrating = document.getElementById('repo_migrating'); + if (!repoMigrating) return; - $('#repo_migrating_retry').on('click', doMigrationRetry); + document.getElementById('repo_migrating_retry').addEventListener('click', doMigrationRetry); - const task = $repoMigrating.attr('data-migrating-task-id'); + const task = repoMigrating.getAttribute('data-migrating-task-id'); - // returns true if the refresh still need to be called after a while + // returns true if the refresh still needs to be called after a while const refresh = async () => { const res = await GET(`${appSubUrl}/user/task/${task}`); if (res.status !== 200) return true; // continue to refresh if network error occurs @@ -21,7 +20,7 @@ export function initRepoMigrationStatusChecker() { // for all status if (data.message) { - $('#repo_migrating_progress_message').text(data.message); + document.getElementById('repo_migrating_progress_message').textContent = data.message; } // TaskStatusFinished @@ -37,7 +36,7 @@ export function initRepoMigrationStatusChecker() { showElem('#repo_migrating_retry'); showElem('#repo_migrating_failed'); showElem('#repo_migrating_failed_image'); - $('#repo_migrating_failed_error').text(data.message); + document.getElementById('repo_migrating_failed_error').textContent = data.message; return false; } @@ -59,6 +58,6 @@ export function initRepoMigrationStatusChecker() { } async function doMigrationRetry(e) { - await POST($(e.target).attr('data-migrating-task-retry-url')); + await POST(e.target.getAttribute('data-migrating-task-retry-url')); window.location.reload(); } diff --git a/web_src/js/features/repo-migration.js b/web_src/js/features/repo-migration.js index 3bd0e6d72c..59e282e4e7 100644 --- a/web_src/js/features/repo-migration.js +++ b/web_src/js/features/repo-migration.js @@ -1,38 +1,42 @@ -import $ from 'jquery'; import {hideElem, showElem, toggleElem} from '../utils/dom.js'; -const $service = $('#service_type'); -const $user = $('#auth_username'); -const $pass = $('#auth_password'); -const $token = $('#auth_token'); -const $mirror = $('#mirror'); -const $lfs = $('#lfs'); -const $lfsSettings = $('#lfs_settings'); -const $lfsEndpoint = $('#lfs_endpoint'); -const $items = $('#migrate_items').find('input[type=checkbox]'); +const service = document.getElementById('service_type'); +const user = document.getElementById('auth_username'); +const pass = document.getElementById('auth_password'); +const token = document.getElementById('auth_token'); +const mirror = document.getElementById('mirror'); +const lfs = document.getElementById('lfs'); +const lfsSettings = document.getElementById('lfs_settings'); +const lfsEndpoint = document.getElementById('lfs_endpoint'); +const items = document.querySelectorAll('#migrate_items input[type=checkbox]'); export function initRepoMigration() { checkAuth(); setLFSSettingsVisibility(); - $user.on('input', () => {checkItems(false)}); - $pass.on('input', () => {checkItems(false)}); - $token.on('input', () => {checkItems(true)}); - $mirror.on('change', () => {checkItems(true)}); - $('#lfs_settings_show').on('click', () => { showElem($lfsEndpoint); return false }); - $lfs.on('change', setLFSSettingsVisibility); + user?.addEventListener('input', () => {checkItems(false)}); + pass?.addEventListener('input', () => {checkItems(false)}); + token?.addEventListener('input', () => {checkItems(true)}); + mirror?.addEventListener('change', () => {checkItems(true)}); + document.getElementById('lfs_settings_show')?.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + showElem(lfsEndpoint); + }); + lfs?.addEventListener('change', setLFSSettingsVisibility); - const $cloneAddr = $('#clone_addr'); - $cloneAddr.on('change', () => { - const $repoName = $('#repo_name'); - if ($cloneAddr.val().length > 0 && $repoName.val().length === 0) { // Only modify if repo_name input is blank - $repoName.val($cloneAddr.val().match(/^(.*\/)?((.+?)(\.git)?)$/)[3]); + const cloneAddr = document.getElementById('clone_addr'); + cloneAddr?.addEventListener('change', () => { + const repoName = document.getElementById('repo_name'); + if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank + repoName.value = cloneAddr.value.match(/^(.*\/)?((.+?)(\.git)?)$/)[3]; } }); } function checkAuth() { - const serviceType = $service.val(); + if (!service) return; + const serviceType = Number(service.value); checkItems(serviceType !== 1); } @@ -40,24 +44,26 @@ function checkAuth() { function checkItems(tokenAuth) { let enableItems; if (tokenAuth) { - enableItems = $token.val() !== ''; + enableItems = token?.value !== ''; } else { - enableItems = $user.val() !== '' || $pass.val() !== ''; + enableItems = user?.value !== '' || pass?.value !== ''; } - if (enableItems && $service.val() > 1) { - if ($mirror.is(':checked')) { - $items.not('[name="wiki"]').attr('disabled', true); - $items.filter('[name="wiki"]').attr('disabled', false); + if (enableItems && Number(service?.value) > 1) { + if (mirror?.checked) { + for (const item of items) { + item.disabled = item.name !== 'wiki'; + } return; } - $items.attr('disabled', false); + for (const item of items) item.disabled = false; } else { - $items.attr('disabled', true); + for (const item of items) item.disabled = true; } } function setLFSSettingsVisibility() { - const visible = $lfs.is(':checked'); - toggleElem($lfsSettings, visible); - hideElem($lfsEndpoint); + if (!lfs) return; + const visible = lfs.checked; + toggleElem(lfsSettings, visible); + hideElem(lfsEndpoint); } diff --git a/web_src/js/features/repo-projects.js b/web_src/js/features/repo-projects.js index 5a2a7e72ef..a869c24c82 100644 --- a/web_src/js/features/repo-projects.js +++ b/web_src/js/features/repo-projects.js @@ -1,9 +1,8 @@ import $ from 'jquery'; -import {useLightTextOnBackground} from '../utils/color.js'; -import tinycolor from 'tinycolor2'; +import {contrastColor} from '../utils/color.js'; import {createSortable} from '../modules/sortable.js'; - -const {csrfToken} = window.config; +import {POST, DELETE, PUT} from '../modules/fetch.js'; +import tinycolor from 'tinycolor2'; function updateIssueCount(cards) { const parent = cards.parentElement; @@ -11,45 +10,42 @@ function updateIssueCount(cards) { parent.getElementsByClassName('project-column-issue-count')[0].textContent = cnt; } -function createNewColumn(url, columnTitle, projectColorInput) { - $.ajax({ - url, - data: JSON.stringify({title: columnTitle.val(), color: projectColorInput.val()}), - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - method: 'POST', - }).done(() => { +async function createNewColumn(url, columnTitle, projectColorInput) { + try { + await POST(url, { + data: { + title: columnTitle.val(), + color: projectColorInput.val(), + }, + }); + } catch (error) { + console.error(error); + } finally { columnTitle.closest('form').removeClass('dirty'); window.location.reload(); - }); + } } -function moveIssue({item, from, to, oldIndex}) { +async function moveIssue({item, from, to, oldIndex}) { const columnCards = to.getElementsByClassName('issue-card'); updateIssueCount(from); updateIssueCount(to); const columnSorting = { issues: Array.from(columnCards, (card, i) => ({ - issueID: parseInt($(card).attr('data-issue')), + issueID: parseInt(card.getAttribute('data-issue')), sorting: i, })), }; - $.ajax({ - url: `${to.getAttribute('data-url')}/move`, - data: JSON.stringify(columnSorting), - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - type: 'POST', - error: () => { - from.insertBefore(item, from.children[oldIndex]); - }, - }); + try { + await POST(`${to.getAttribute('data-url')}/move`, { + data: columnSorting, + }); + } catch (error) { + console.error(error); + from.insertBefore(item, from.children[oldIndex]); + } } async function initRepoProjectSortable() { @@ -62,25 +58,21 @@ async function initRepoProjectSortable() { createSortable(mainBoard, { group: 'project-column', draggable: '.project-column', - filter: '[data-id="0"]', - animation: 150, - ghostClass: 'card-ghost', + handle: '.project-column-header', delayOnTouchOnly: true, delay: 500, - onSort: () => { + onSort: async () => { boardColumns = mainBoard.getElementsByClassName('project-column'); for (let i = 0; i < boardColumns.length; i++) { const column = boardColumns[i]; - if (parseInt($(column).data('sorting')) !== i) { - $.ajax({ - url: $(column).data('url'), - data: JSON.stringify({sorting: i, color: rgbToHex($(column).css('backgroundColor'))}), - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - method: 'PUT', - }); + if (parseInt(column.getAttribute('data-sorting')) !== i) { + try { + const bgColor = column.style.backgroundColor; // will be rgb() string + const color = bgColor ? tinycolor(bgColor).toHexString() : ''; + await PUT(column.getAttribute('data-url'), {data: {sorting: i, color}}); + } catch (error) { + console.error(error); + } } } }, @@ -90,8 +82,6 @@ async function initRepoProjectSortable() { const boardCardList = boardColumn.getElementsByClassName('cards')[0]; createSortable(boardCardList, { group: 'shared', - animation: 150, - ghostClass: 'card-ghost', onAdd: moveIssue, onUpdate: moveIssue, delayOnTouchOnly: true, @@ -101,115 +91,96 @@ async function initRepoProjectSortable() { } export function initRepoProject() { - if (!$('.repository.projects').length) { + if (!document.querySelector('.repository.projects')) { return; } const _promise = initRepoProjectSortable(); - $('.edit-project-column-modal').each(function () { - const projectHeader = $(this).closest('.project-column-header'); - const projectTitleLabel = projectHeader.find('.project-column-title'); - const projectTitleInput = $(this).find('.project-column-title-input'); - const projectColorInput = $(this).find('#new_project_column_color'); - const boardColumn = $(this).closest('.project-column'); - - if (boardColumn.css('backgroundColor')) { - setLabelColor(projectHeader, rgbToHex(boardColumn.css('backgroundColor'))); - } - - $(this).find('.edit-project-column-button').on('click', function (e) { + for (const modal of document.getElementsByClassName('edit-project-column-modal')) { + const projectHeader = modal.closest('.project-column-header'); + const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label'); + const projectTitleInput = modal.querySelector('.project-column-title-input'); + const projectColorInput = modal.querySelector('#new_project_column_color'); + const boardColumn = modal.closest('.project-column'); + modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) { e.preventDefault(); - - $.ajax({ - url: $(this).data('url'), - data: JSON.stringify({title: projectTitleInput.val(), color: projectColorInput.val()}), - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - method: 'PUT', - }).done(() => { - projectTitleLabel.text(projectTitleInput.val()); - projectTitleInput.closest('form').removeClass('dirty'); - if (projectColorInput.val()) { - setLabelColor(projectHeader, projectColorInput.val()); + try { + await PUT(this.getAttribute('data-url'), { + data: { + title: projectTitleInput?.value, + color: projectColorInput?.value, + }, + }); + } catch (error) { + console.error(error); + } finally { + projectTitleLabel.textContent = projectTitleInput?.value; + projectTitleInput.closest('form')?.classList.remove('dirty'); + const dividers = boardColumn.querySelectorAll(':scope > .divider'); + if (projectColorInput.value) { + const color = contrastColor(projectColorInput.value); + boardColumn.style.setProperty('background', projectColorInput.value, 'important'); + boardColumn.style.setProperty('color', color, 'important'); + for (const divider of dividers) { + divider.style.setProperty('color', color); + } + } else { + boardColumn.style.removeProperty('background'); + boardColumn.style.removeProperty('color'); + for (const divider of dividers) { + divider.style.removeProperty('color'); + } } - boardColumn.attr('style', `background: ${projectColorInput.val()}!important`); $('.ui.modal').modal('hide'); - }); + } }); - }); + } $('.default-project-column-modal').each(function () { - const boardColumn = $(this).closest('.project-column'); - const showButton = $(boardColumn).find('.default-project-column-show'); - const commitButton = $(this).find('.actions > .ok.button'); + const $boardColumn = $(this).closest('.project-column'); + const $showButton = $($boardColumn).find('.default-project-column-show'); + const $commitButton = $(this).find('.actions > .ok.button'); - $(commitButton).on('click', (e) => { + $($commitButton).on('click', async (e) => { e.preventDefault(); - $.ajax({ - method: 'POST', - url: $(showButton).data('url'), - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - }).done(() => { + try { + await POST($($showButton).data('url')); + } catch (error) { + console.error(error); + } finally { window.location.reload(); - }); + } }); }); $('.show-delete-project-column-modal').each(function () { - const deleteColumnModal = $(`${$(this).attr('data-modal')}`); - const deleteColumnButton = deleteColumnModal.find('.actions > .ok.button'); - const deleteUrl = $(this).attr('data-url'); + const $deleteColumnModal = $(`${this.getAttribute('data-modal')}`); + const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button'); + const deleteUrl = this.getAttribute('data-url'); - deleteColumnButton.on('click', (e) => { + $deleteColumnButton.on('click', async (e) => { e.preventDefault(); - $.ajax({ - url: deleteUrl, - headers: { - 'X-Csrf-Token': csrfToken, - }, - contentType: 'application/json', - method: 'DELETE', - }).done(() => { + try { + await DELETE(deleteUrl); + } catch (error) { + console.error(error); + } finally { window.location.reload(); - }); + } }); }); $('#new_project_column_submit').on('click', (e) => { e.preventDefault(); - const columnTitle = $('#new_project_column'); - const projectColorInput = $('#new_project_column_color_picker'); - if (!columnTitle.val()) { + const $columnTitle = $('#new_project_column'); + const $projectColorInput = $('#new_project_column_color_picker'); + if (!$columnTitle.val()) { return; } - const url = $(this).data('url'); - createNewColumn(url, columnTitle, projectColorInput); + const url = e.target.getAttribute('data-url'); + createNewColumn(url, $columnTitle, $projectColorInput); }); } - -function setLabelColor(label, color) { - const {r, g, b} = tinycolor(color).toRgb(); - if (useLightTextOnBackground(r, g, b)) { - label.removeClass('dark-label').addClass('light-label'); - } else { - label.removeClass('light-label').addClass('dark-label'); - } -} - -function rgbToHex(rgb) { - rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+).*\)$/); - return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`; -} - -function hex(x) { - const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; - return Number.isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16]; -} diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js index 3338c2874b..f3cfa74418 100644 --- a/web_src/js/features/repo-release.js +++ b/web_src/js/features/repo-release.js @@ -1,19 +1,19 @@ -import $ from 'jquery'; import {hideElem, showElem} from '../utils/dom.js'; import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; export function initRepoRelease() { - $(document).on('click', '.remove-rel-attach', function() { - const uuid = $(this).data('uuid'); - const id = $(this).data('id'); - $(`input[name='attachment-del-${uuid}']`).attr('value', true); - hideElem($(`#attachment-${id}`)); + document.addEventListener('click', (e) => { + if (e.target.matches('.remove-rel-attach')) { + const uuid = e.target.getAttribute('data-uuid'); + const id = e.target.getAttribute('data-id'); + document.querySelector(`input[name='attachment-del-${uuid}']`).value = 'true'; + hideElem(`#attachment-${id}`); + } }); } export function initRepoReleaseNew() { - const $repoReleaseNew = $('.repository.new.release'); - if (!$repoReleaseNew.length) return; + if (!document.querySelector('.repository.new.release')) return; initTagNameEditor(); initRepoReleaseEditor(); @@ -30,8 +30,9 @@ function initTagNameEditor() { const newTagHelperText = el.getAttribute('data-tag-helper-new'); const existingTagHelperText = el.getAttribute('data-tag-helper-existing'); - document.getElementById('tag-name').addEventListener('keyup', (e) => { - const value = e.target.value; + const tagNameInput = document.getElementById('tag-name'); + const hideTargetInput = function(tagNameInput) { + const value = tagNameInput.value; const tagHelper = document.getElementById('tag-helper'); if (existingTags.includes(value)) { // If the tag already exists, hide the target branch selector. @@ -41,13 +42,17 @@ function initTagNameEditor() { showElem('#tag-target-selector'); tagHelper.textContent = value ? newTagHelperText : defaultTagHelperText; } + }; + hideTargetInput(tagNameInput); // update on page load because the input may have a value + tagNameInput.addEventListener('input', (e) => { + hideTargetInput(e.target); }); } function initRepoReleaseEditor() { - const $editor = $('.repository.new.release .combo-markdown-editor'); - if ($editor.length === 0) { + const editor = document.querySelector('.repository.new.release .combo-markdown-editor'); + if (!editor) { return; } - const _promise = initComboMarkdownEditor($editor); + initComboMarkdownEditor(editor); } diff --git a/web_src/js/features/repo-search.js b/web_src/js/features/repo-search.js new file mode 100644 index 0000000000..185f6119d9 --- /dev/null +++ b/web_src/js/features/repo-search.js @@ -0,0 +1,22 @@ +export function initRepositorySearch() { + const repositorySearchForm = document.querySelector('#repo-search-form'); + if (!repositorySearchForm) return; + + repositorySearchForm.addEventListener('change', (e) => { + e.preventDefault(); + + const formData = new FormData(repositorySearchForm); + const params = new URLSearchParams(formData); + + if (e.target.name === 'clear-filter') { + params.delete('archived'); + params.delete('fork'); + params.delete('mirror'); + params.delete('template'); + params.delete('private'); + } + + params.delete('clear-filter'); + window.location.search = params.toString(); + }); +} diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js index 75e624a6a7..52c5de2bfa 100644 --- a/web_src/js/features/repo-settings.js +++ b/web_src/js/features/repo-settings.js @@ -2,27 +2,29 @@ import $ from 'jquery'; import {minimatch} from 'minimatch'; import {createMonaco} from './codeeditor.js'; import {onInputDebounce, toggleElem} from '../utils/dom.js'; +import {POST} from '../modules/fetch.js'; const {appSubUrl, csrfToken} = window.config; export function initRepoSettingsCollaboration() { // Change collaborator access mode - $('.page-content.repository .ui.dropdown.access-mode').each((_, e) => { - const $dropdown = $(e); + $('.page-content.repository .ui.dropdown.access-mode').each((_, el) => { + const $dropdown = $(el); const $text = $dropdown.find('> .text'); $dropdown.dropdown({ - action(_text, value) { - const lastValue = $dropdown.attr('data-last-value'); - $.post($dropdown.attr('data-url'), { - _csrf: csrfToken, - uid: $dropdown.attr('data-uid'), - mode: value, - }).fail(() => { + async action(_text, value) { + const lastValue = el.getAttribute('data-last-value'); + try { + el.setAttribute('data-last-value', value); + $dropdown.dropdown('hide'); + const data = new FormData(); + data.append('uid', el.getAttribute('data-uid')); + data.append('mode', value); + await POST(el.getAttribute('data-url'), {data}); + } catch { $text.text('(error)'); // prevent from misleading users when error occurs - $dropdown.attr('data-last-value', lastValue); - }); - $dropdown.attr('data-last-value', value); - $dropdown.dropdown('hide'); + el.setAttribute('data-last-value', lastValue); + } }, onChange(_value, text, _$choice) { $text.text(text); // update the text when using keyboard navigating @@ -30,61 +32,69 @@ export function initRepoSettingsCollaboration() { onHide() { // set to the really selected value, defer to next tick to make sure `action` has finished its work because the calling order might be onHide -> action setTimeout(() => { - const $item = $dropdown.dropdown('get item', $dropdown.attr('data-last-value')); + const $item = $dropdown.dropdown('get item', el.getAttribute('data-last-value')); if ($item) { - $dropdown.dropdown('set selected', $dropdown.attr('data-last-value')); + $dropdown.dropdown('set selected', el.getAttribute('data-last-value')); } else { $text.text('(none)'); // prevent from misleading users when the access mode is undefined } }, 0); - } + }, }); }); } export function initRepoSettingSearchTeamBox() { - const $searchTeamBox = $('#search-team-box'); - $searchTeamBox.search({ + const searchTeamBox = document.getElementById('search-team-box'); + if (!searchTeamBox) return; + + $(searchTeamBox).search({ minCharacters: 2, apiSettings: { - url: `${appSubUrl}/org/${$searchTeamBox.attr('data-org-name')}/teams/-/search?q={query}`, + url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`, headers: {'X-Csrf-Token': csrfToken}, onResponse(response) { const items = []; $.each(response.data, (_i, item) => { items.push({ title: item.name, - description: `${item.permission} access` // TODO: translate this string + description: `${item.permission} access`, // TODO: translate this string }); }); return {results: items}; - } + }, }, searchFields: ['name', 'description'], - showNoResults: false + showNoResults: false, }); } export function initRepoSettingGitHook() { - if ($('.edit.githook').length === 0) return; + if (!$('.edit.githook').length) return; const filename = document.querySelector('.hook-filename').textContent; const _promise = createMonaco($('#content')[0], filename, {language: 'shell'}); } export function initRepoSettingBranches() { - if (!$('.repository.settings.branches').length) return; - $('.toggle-target-enabled').on('change', function () { - const $target = $($(this).attr('data-target')); - $target.toggleClass('disabled', !this.checked); - }); - $('.toggle-target-disabled').on('change', function () { - const $target = $($(this).attr('data-target')); - if (this.checked) $target.addClass('disabled'); // only disable, do not auto enable - }); - $('#dismiss_stale_approvals').on('change', function () { - const $target = $('#ignore_stale_approvals_box'); - $target.toggleClass('disabled', this.checked); + if (!document.querySelector('.repository.settings.branches')) return; + + for (const el of document.getElementsByClassName('toggle-target-enabled')) { + el.addEventListener('change', function () { + const target = document.querySelector(this.getAttribute('data-target')); + target?.classList.toggle('disabled', !this.checked); + }); + } + + for (const el of document.getElementsByClassName('toggle-target-disabled')) { + el.addEventListener('change', function () { + const target = document.querySelector(this.getAttribute('data-target')); + if (this.checked) target?.classList.add('disabled'); // only disable, do not auto enable + }); + } + + document.getElementById('dismiss_stale_approvals')?.addEventListener('change', function () { + document.getElementById('ignore_stale_approvals_box')?.classList.toggle('disabled', this.checked); }); // show the `Matched` mark for the status checks that match the pattern @@ -102,7 +112,6 @@ export function initRepoSettingBranches() { break; } } - toggleElem(el, matched); } }; diff --git a/web_src/js/features/repo-template.js b/web_src/js/features/repo-template.js index 1e83e74780..5f63e8b3ba 100644 --- a/web_src/js/features/repo-template.js +++ b/web_src/js/features/repo-template.js @@ -29,13 +29,13 @@ export function initRepoTemplateSearch() { const filteredResponse = {success: true, results: []}; filteredResponse.results.push({ name: '', - value: '' + value: '', }); // Parse the response from the api to work with our dropdown $.each(response.data, (_r, repo) => { filteredResponse.results.push({ name: htmlEscape(repo.repository.full_name), - value: repo.repository.id + value: repo.repository.id, }); }); return filteredResponse; @@ -43,7 +43,7 @@ export function initRepoTemplateSearch() { cache: false, }, - fullTextSearch: true + fullTextSearch: true, }); }; $('#uid').on('change', changeOwner); diff --git a/web_src/js/features/repo-unicode-escape.js b/web_src/js/features/repo-unicode-escape.js index 6a201ec4d1..d878532001 100644 --- a/web_src/js/features/repo-unicode-escape.js +++ b/web_src/js/features/repo-unicode-escape.js @@ -1,31 +1,27 @@ -import $ from 'jquery'; -import {hideElem, showElem} from '../utils/dom.js'; +import {hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.js'; export function initUnicodeEscapeButton() { - $(document).on('click', '.escape-button', (e) => { + document.addEventListener('click', (e) => { + const btn = e.target.closest('.escape-button, .unescape-button, .toggle-escape-button'); + if (!btn) return; + e.preventDefault(); - $(e.target).parents('.file-content, .non-diff-file-content').find('.file-code, .file-view').addClass('unicode-escaped'); - hideElem($(e.target)); - showElem($(e.target).siblings('.unescape-button')); - }); - $(document).on('click', '.unescape-button', (e) => { - e.preventDefault(); - $(e.target).parents('.file-content, .non-diff-file-content').find('.file-code, .file-view').removeClass('unicode-escaped'); - hideElem($(e.target)); - showElem($(e.target).siblings('.escape-button')); - }); - $(document).on('click', '.toggle-escape-button', (e) => { - e.preventDefault(); - const fileContent = $(e.target).parents('.file-content, .non-diff-file-content'); - const fileView = fileContent.find('.file-code, .file-view'); - if (fileView.hasClass('unicode-escaped')) { - fileView.removeClass('unicode-escaped'); - hideElem(fileContent.find('.unescape-button')); - showElem(fileContent.find('.escape-button')); - } else { - fileView.addClass('unicode-escaped'); - showElem(fileContent.find('.unescape-button')); - hideElem(fileContent.find('.escape-button')); + + const fileContent = btn.closest('.file-content, .non-diff-file-content'); + const fileView = fileContent?.querySelectorAll('.file-code, .file-view'); + if (btn.matches('.escape-button')) { + for (const el of fileView) el.classList.add('unicode-escaped'); + hideElem(btn); + showElem(queryElemSiblings(btn, '.unescape-button')); + } else if (btn.matches('.unescape-button')) { + for (const el of fileView) el.classList.remove('unicode-escaped'); + hideElem(btn); + showElem(queryElemSiblings(btn, '.escape-button')); + } else if (btn.matches('.toggle-escape-button')) { + const isEscaped = fileView[0]?.classList.contains('unicode-escaped'); + for (const el of fileView) el.classList.toggle('unicode-escaped', !isEscaped); + toggleElem(fileContent.querySelectorAll('.unescape-button'), !isEscaped); + toggleElem(fileContent.querySelectorAll('.escape-button'), isEscaped); } }); } diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js index 58036fde37..03a2c68c5a 100644 --- a/web_src/js/features/repo-wiki.js +++ b/web_src/js/features/repo-wiki.js @@ -1,50 +1,51 @@ -import $ from 'jquery'; import {initMarkupContent} from '../markup/content.js'; import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; import {fomanticMobileScreen} from '../modules/fomantic.js'; - -const {csrfToken} = window.config; +import {POST} from '../modules/fetch.js'; async function initRepoWikiFormEditor() { - const $editArea = $('.repository.wiki .combo-markdown-editor textarea'); - if (!$editArea.length) return; + const editArea = document.querySelector('.repository.wiki .combo-markdown-editor textarea'); + if (!editArea) return; - const $form = $('.repository.wiki.new .ui.form'); - const $editorContainer = $form.find('.combo-markdown-editor'); + const form = document.querySelector('.repository.wiki.new .ui.form'); + const editorContainer = form.querySelector('.combo-markdown-editor'); let editor; let renderRequesting = false; let lastContent; - const renderEasyMDEPreview = function () { + const renderEasyMDEPreview = async function () { if (renderRequesting) return; - const $previewFull = $editorContainer.find('.EasyMDEContainer .editor-preview-active'); - const $previewSide = $editorContainer.find('.EasyMDEContainer .editor-preview-active-side'); - const $previewTarget = $previewSide.length ? $previewSide : $previewFull; - const newContent = $editArea.val(); - if (editor && $previewTarget.length && lastContent !== newContent) { + const previewFull = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active'); + const previewSide = editorContainer.querySelector('.EasyMDEContainer .editor-preview-active-side'); + const previewTarget = previewSide || previewFull; + const newContent = editArea.value; + if (editor && previewTarget && lastContent !== newContent) { renderRequesting = true; - $.post(editor.previewUrl, { - _csrf: csrfToken, - mode: editor.previewMode, - context: editor.previewContext, - text: newContent, - wiki: editor.previewWiki, - }).done((data) => { + const formData = new FormData(); + formData.append('mode', editor.previewMode); + formData.append('context', editor.previewContext); + formData.append('text', newContent); + formData.append('wiki', editor.previewWiki); + try { + const response = await POST(editor.previewUrl, {data: formData}); + const data = await response.text(); lastContent = newContent; - $previewTarget.html(`<div class="markup ui segment">${data}</div>`); + previewTarget.innerHTML = `<div class="markup ui segment">${data}</div>`; initMarkupContent(); - }).always(() => { + } catch (error) { + console.error('Error rendering preview:', error); + } finally { renderRequesting = false; setTimeout(renderEasyMDEPreview, 1000); - }); + } } else { setTimeout(renderEasyMDEPreview, 1000); } }; renderEasyMDEPreview(); - editor = await initComboMarkdownEditor($editorContainer, { + editor = await initComboMarkdownEditor(editorContainer, { useScene: 'wiki', // EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it. // And another benefit is that we only need to write the style once for both editors. @@ -59,14 +60,15 @@ async function initRepoWikiFormEditor() { 'gitea-code-inline', 'code', 'quote', '|', 'gitea-checkbox-empty', 'gitea-checkbox-checked', '|', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'table', 'horizontal-rule', '|', - 'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea' + 'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea', ], }, }); - $form.on('submit', () => { - if (!validateTextareaNonEmpty($editArea)) { - return false; + form.addEventListener('submit', (e) => { + if (!validateTextareaNonEmpty(editArea)) { + e.preventDefault(); + e.stopPropagation(); } }); } diff --git a/web_src/js/features/sshkey-helper.js b/web_src/js/features/sshkey-helper.js index 099b54d3a6..3960eefe8e 100644 --- a/web_src/js/features/sshkey-helper.js +++ b/web_src/js/features/sshkey-helper.js @@ -1,12 +1,10 @@ -import $ from 'jquery'; - export function initSshKeyFormParser() { -// Parse SSH Key - $('#ssh-key-content').on('change paste keyup', function () { - const arrays = $(this).val().split(' '); - const $title = $('#ssh-key-title'); - if ($title.val() === '' && arrays.length === 3 && arrays[2] !== '') { - $title.val(arrays[2]); + // Parse SSH Key + document.getElementById('ssh-key-content')?.addEventListener('input', function () { + const arrays = this.value.split(' '); + const title = document.getElementById('ssh-key-title'); + if (!title.value && arrays.length === 3 && arrays[2] !== '') { + title.value = arrays[2]; } }); } diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js index f43014fec5..2ec74344fc 100644 --- a/web_src/js/features/stopwatch.js +++ b/web_src/js/features/stopwatch.js @@ -1,8 +1,9 @@ -import $ from 'jquery'; import prettyMilliseconds from 'pretty-ms'; import {createTippy} from '../modules/tippy.js'; +import {GET} from '../modules/fetch.js'; +import {hideElem, showElem} from '../utils/dom.js'; -const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; export function initStopwatch() { if (!enableTimeTracking) { @@ -28,7 +29,7 @@ export function initStopwatch() { }); // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used. - const currSeconds = $('.stopwatch-time').attr('data-seconds'); + const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds'); if (currSeconds) { updateStopwatchTime(currSeconds); } @@ -74,7 +75,7 @@ export function initStopwatch() { type: 'close', }); worker.port.close(); - window.location.href = appSubUrl; + window.location.href = `${appSubUrl}/`; } else if (event.data.type === 'close') { worker.port.postMessage({ type: 'close', @@ -112,29 +113,31 @@ async function updateStopwatchWithCallback(callback, timeout) { } async function updateStopwatch() { - const data = await $.ajax({ - type: 'GET', - url: `${appSubUrl}/user/stopwatches`, - headers: {'X-Csrf-Token': csrfToken}, - }); + const response = await GET(`${appSubUrl}/user/stopwatches`); + if (!response.ok) { + console.error('Failed to fetch stopwatch data'); + return false; + } + const data = await response.json(); return updateStopwatchData(data); } function updateStopwatchData(data) { const watch = data[0]; - const btnEl = $('.active-stopwatch-trigger'); + const btnEl = document.querySelector('.active-stopwatch-trigger'); if (!watch) { clearStopwatchTimer(); - btnEl.addClass('gt-hidden'); + hideElem(btnEl); } else { const {repo_owner_name, repo_name, issue_index, seconds} = watch; const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`; - $('.stopwatch-link').attr('href', issueUrl); - $('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`); - $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`); - $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`); + document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl); + document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/toggle`); + document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`); + const stopwatchIssue = document.querySelector('.stopwatch-issue'); + if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`; updateStopwatchTime(seconds); - btnEl.removeClass('gt-hidden'); + showElem(btnEl); } return Boolean(data.length); } @@ -151,12 +154,13 @@ function updateStopwatchTime(seconds) { if (!Number.isFinite(secs)) return; clearStopwatchTimer(); - const $stopwatch = $('.stopwatch-time'); + const stopwatch = document.querySelector('.stopwatch-time'); + // TODO: replace with <relative-time> similar to how system status up time is shown const start = Date.now(); const updateUi = () => { const delta = Date.now() - start; const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true}); - $stopwatch.text(dur); + if (stopwatch) stopwatch.textContent = dur; }; updateUi(); updateTimeIntervalId = setInterval(updateUi, 1000); diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js index 055777be79..02cd484374 100644 --- a/web_src/js/features/tribute.js +++ b/web_src/js/features/tribute.js @@ -25,7 +25,7 @@ function makeCollections({mentions, emoji}) { }, menuItemTemplate: (item) => { return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`; - } + }, }); } @@ -36,12 +36,12 @@ function makeCollections({mentions, emoji}) { menuItemTemplate: (item) => { return ` <div class="tribute-item"> - <img src="${htmlEscape(item.original.avatar)}" class="gt-mr-3"/> + <img src="${htmlEscape(item.original.avatar)}" class="tw-mr-2"/> <span class="name">${htmlEscape(item.original.name)}</span> ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''} </div> `; - } + }, }); } diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index 363e039760..6dfbb4d765 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -26,7 +26,7 @@ export async function initUserAuthWebAuthn() { } try { const credential = await navigator.credentials.get({ - publicKey: options.publicKey + publicKey: options.publicKey, }); await verifyAssertion(credential); } catch (err) { @@ -37,7 +37,7 @@ export async function initUserAuthWebAuthn() { delete options.publicKey.extensions.appid; try { const credential = await navigator.credentials.get({ - publicKey: options.publicKey + publicKey: options.publicKey, }); await verifyAssertion(credential); } catch (err) { @@ -185,7 +185,7 @@ async function webAuthnRegisterRequest() { try { const credential = await navigator.credentials.create({ - publicKey: options.publicKey + publicKey: options.publicKey, }); await webauthnRegistered(credential); } catch (err) { diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js index af380dcfc7..a871ac471c 100644 --- a/web_src/js/features/user-auth.js +++ b/web_src/js/features/user-auth.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {checkAppUrl} from './common-global.js'; export function initUserAuthOauth2() { @@ -10,41 +9,14 @@ export function initUserAuthOauth2() { for (const link of outer.querySelectorAll('.oauth-login-link')) { link.addEventListener('click', () => { - inner.classList.add('gt-invisible'); + inner.classList.add('tw-invisible'); outer.classList.add('is-loading'); setTimeout(() => { // recover previous content to let user try again // usually redirection will be performed before this action outer.classList.remove('is-loading'); - inner.classList.remove('gt-invisible'); + inner.classList.remove('tw-invisible'); }, 5000); }); } } - -export function initUserAuthLinkAccountView() { - const $lnkUserPage = $('.page-content.user.link-account'); - if ($lnkUserPage.length === 0) { - return false; - } - - const $signinTab = $lnkUserPage.find('.item[data-tab="auth-link-signin-tab"]'); - const $signUpTab = $lnkUserPage.find('.item[data-tab="auth-link-signup-tab"]'); - const $signInView = $lnkUserPage.find('.tab[data-tab="auth-link-signin-tab"]'); - const $signUpView = $lnkUserPage.find('.tab[data-tab="auth-link-signup-tab"]'); - - $signUpTab.on('click', () => { - $signinTab.removeClass('active'); - $signInView.removeClass('active'); - $signUpTab.addClass('active'); - $signUpView.addClass('active'); - return false; - }); - - $signinTab.on('click', () => { - $signUpTab.removeClass('active'); - $signUpView.removeClass('active'); - $signinTab.addClass('active'); - $signInView.addClass('active'); - }); -} diff --git a/web_src/js/features/user-settings.js b/web_src/js/features/user-settings.js index d49bf39275..2d8c53e457 100644 --- a/web_src/js/features/user-settings.js +++ b/web_src/js/features/user-settings.js @@ -1,18 +1,19 @@ -import $ from 'jquery'; import {hideElem, showElem} from '../utils/dom.js'; export function initUserSettings() { - if ($('.user.settings.profile').length > 0) { - $('#username').on('keyup', function () { - const $prompt = $('#name-change-prompt'); - const $prompt_redirect = $('#name-change-redirect-prompt'); - if ($(this).val().toString().toLowerCase() !== $(this).data('name').toString().toLowerCase()) { - showElem($prompt); - showElem($prompt_redirect); - } else { - hideElem($prompt); - hideElem($prompt_redirect); - } - }); - } + if (!document.querySelectorAll('.user.settings.profile').length) return; + + const usernameInput = document.getElementById('username'); + if (!usernameInput) return; + usernameInput.addEventListener('input', function () { + const prompt = document.getElementById('name-change-prompt'); + const promptRedirect = document.getElementById('name-change-redirect-prompt'); + if (this.value.toLowerCase() !== this.getAttribute('data-name').toLowerCase()) { + showElem(prompt); + showElem(promptRedirect); + } else { + hideElem(prompt); + hideElem(promptRedirect); + } + }); } diff --git a/web_src/js/htmx.js b/web_src/js/htmx.js index 92400d1cbe..5ca3018308 100644 --- a/web_src/js/htmx.js +++ b/web_src/js/htmx.js @@ -1,6 +1,9 @@ import * as htmx from 'htmx.org'; import {showErrorToast} from './modules/toast.js'; +// https://github.com/bigskysoftware/idiomorph#htmx +import 'idiomorph/dist/idiomorph-ext.js'; + // https://htmx.org/reference/#config htmx.config.requestClass = 'is-loading'; htmx.config.scrollIntoViewOnBoost = false; diff --git a/web_src/js/index.js b/web_src/js/index.js index 4713618506..fc2f6b9b0b 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -13,6 +13,7 @@ import {initImageDiff} from './features/imagediff.js'; import {initRepoMigration} from './features/repo-migration.js'; import {initRepoProject} from './features/repo-projects.js'; import {initTableSort} from './features/tablesort.js'; +import {initAutoFocusEnd} from './features/autofocus-end.js'; import {initAdminUserListSearchForm} from './features/admin/users.js'; import {initAdminConfigs} from './features/admin/config.js'; import {initMarkupAnchors} from './markup/anchors.js'; @@ -23,7 +24,7 @@ import {initFindFileInRepo} from './features/repo-findfile.js'; import {initCommentContent, initMarkupContent} from './markup/content.js'; import {initPdfViewer} from './render/pdf.js'; -import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js'; +import {initUserAuthOauth2} from './features/user-auth.js'; import { initRepoIssueDue, initRepoIssueReferenceRepositorySearch, @@ -33,11 +34,7 @@ import { initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler, } from './features/repo-issue.js'; -import { - initRepoEllipsisButton, - initRepoCommitLastCommitLoader, - initCommitStatuses, -} from './features/repo-commit.js'; +import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.js'; import { initFootLanguageMenu, initGlobalButtonClickOnEnter, @@ -83,8 +80,13 @@ import {initGiteaFomantic} from './modules/fomantic.js'; import {onDomReady} from './utils/dom.js'; import {initRepoIssueList} from './features/repo-issue-list.js'; import {initCommonIssueListQuickGoto} from './features/common-issue-list.js'; +import {initRepoContributors} from './features/contributors.js'; +import {initRepoCodeFrequency} from './features/code-frequency.js'; +import {initRepoRecentCommits} from './features/recent-commits.js'; import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js'; import {initDirAuto} from './modules/dirauto.js'; +import {initRepositorySearch} from './features/repo-search.js'; +import {initColorPickers} from './features/colorpicker.js'; // Init Gitea's Fomantic settings initGiteaFomantic(); @@ -122,6 +124,7 @@ onDomReady(() => { initSshKeyFormParser(); initStopwatch(); initTableSort(); + initAutoFocusEnd(); initFindFileInRepo(); initCopyContent(); @@ -145,7 +148,6 @@ onDomReady(() => { initRepoCommentForm(); initRepoEllipsisButton(); initRepoDiffCommitBranchesAndTags(); - initRepoCommitLastCommitLoader(); initRepoEditor(); initRepoGraphGit(); initRepoIssueContentHistory(); @@ -172,11 +174,14 @@ onDomReady(() => { initRepoWikiForm(); initRepository(); initRepositoryActionView(); + initRepositorySearch(); + initRepoContributors(); + initRepoCodeFrequency(); + initRepoRecentCommits(); initCommitStatuses(); initCaptcha(); - initUserAuthLinkAccountView(); initUserAuthOauth2(); initUserAuthWebAuthn(); initUserAuthWebAuthnRegister(); @@ -184,4 +189,5 @@ onDomReady(() => { initRepoDiffView(); initPdfViewer(); initScopedAccessTokenCategories(); + initColorPickers(); }); diff --git a/web_src/js/jquery.js b/web_src/js/jquery.js index 892e2763cb..6b2199896c 100644 --- a/web_src/js/jquery.js +++ b/web_src/js/jquery.js @@ -1,3 +1,3 @@ import $ from 'jquery'; -window.$ = window.jQuery = $; +window.$ = window.jQuery = $; // eslint-disable-line no-jquery/variable-pattern diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js index 53dfa2980c..0e2c92713a 100644 --- a/web_src/js/markup/anchors.js +++ b/web_src/js/markup/anchors.js @@ -1,35 +1,70 @@ import {svg} from '../svg.js'; -const headingSelector = '.markup h1, .markup h2, .markup h3, .markup h4, .markup h5, .markup h6'; +const addPrefix = (str) => `user-content-${str}`; +const removePrefix = (str) => str.replace(/^user-content-/, ''); +const hasPrefix = (str) => str.startsWith('user-content-'); -function scrollToAnchor(hash, initial) { - // abort if the browser has already scrolled to another anchor during page load - if (initial && document.querySelector(':target')) return; - if (hash?.length <= 1) return; - const id = decodeURIComponent(hash.substring(1)); - const el = document.getElementById(`user-content-${id}`); - if (el) { - el.scrollIntoView(); - } else if (id.startsWith('user-content-')) { // compat for links with old 'user-content-' prefixed hashes - const el = document.getElementById(id); - if (el) el.scrollIntoView(); +// scroll to anchor while respecting the `user-content` prefix that exists on the target +function scrollToAnchor(encodedId) { + if (!encodedId) return; + const id = decodeURIComponent(encodedId); + const prefixedId = addPrefix(id); + let el = document.getElementById(prefixedId); + + // check for matching user-generated `a[name]` + if (!el) { + const nameAnchors = document.getElementsByName(prefixedId); + if (nameAnchors.length) { + el = nameAnchors[0]; + } } + + // compat for links with old 'user-content-' prefixed hashes + if (!el && hasPrefix(id)) { + return document.getElementById(id)?.scrollIntoView(); + } + + el?.scrollIntoView(); } export function initMarkupAnchors() { - if (!document.querySelector('.markup')) return; + const markupEls = document.querySelectorAll('.markup'); + if (!markupEls.length) return; - for (const heading of document.querySelectorAll(headingSelector)) { - const originalId = heading.id.replace(/^user-content-/, ''); - const a = document.createElement('a'); - a.classList.add('anchor'); - a.setAttribute('href', `#${encodeURIComponent(originalId)}`); - a.innerHTML = svg('octicon-link'); - a.addEventListener('click', (e) => { - scrollToAnchor(e.currentTarget.getAttribute('href'), false); - }); - heading.prepend(a); + for (const markupEl of markupEls) { + // create link icons for markup headings, the resulting link href will remove `user-content-` + for (const heading of markupEl.querySelectorAll('h1, h2, h3, h4, h5, h6')) { + const a = document.createElement('a'); + a.classList.add('anchor'); + a.setAttribute('href', `#${encodeURIComponent(removePrefix(heading.id))}`); + a.innerHTML = svg('octicon-link'); + heading.prepend(a); + } + + // remove `user-content-` prefix from links so they don't show in url bar when clicked + for (const a of markupEl.querySelectorAll('a[href^="#"]')) { + const href = a.getAttribute('href'); + if (!href.startsWith('#user-content-')) continue; + a.setAttribute('href', `#${removePrefix(href.substring(1))}`); + } + + // add `user-content-` prefix to user-generated `a[name]` link targets + // TODO: this prefix should be added in backend instead + for (const a of markupEl.querySelectorAll('a[name]')) { + const name = a.getAttribute('name'); + if (!name) continue; + a.setAttribute('name', addPrefix(a.name)); + } + + for (const a of markupEl.querySelectorAll('a[href^="#"]')) { + a.addEventListener('click', (e) => { + scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1)); + }); + } } - scrollToAnchor(window.location.hash, true); + // scroll to anchor unless the browser has already scrolled somewhere during page load + if (!document.querySelector(':target')) { + scrollToAnchor(window.location.hash?.substring(1)); + } } diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js index 84d88a94c3..0549fb3e31 100644 --- a/web_src/js/markup/mermaid.js +++ b/web_src/js/markup/mermaid.js @@ -45,11 +45,11 @@ export async function renderMermaid() { const {svg} = await mermaid.render('mermaid', source); const iframe = document.createElement('iframe'); - iframe.classList.add('markup-render', 'gt-invisible'); + iframe.classList.add('markup-render', 'tw-invisible'); iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; const mermaidBlock = document.createElement('div'); - mermaidBlock.classList.add('mermaid-block', 'is-loading', 'gt-hidden'); + mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); mermaidBlock.append(iframe); const btn = makeCodeCopyButton(); @@ -58,11 +58,11 @@ export async function renderMermaid() { iframe.addEventListener('load', () => { pre.replaceWith(mermaidBlock); - mermaidBlock.classList.remove('gt-hidden'); + mermaidBlock.classList.remove('tw-hidden'); iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`; setTimeout(() => { // avoid flash of iframe background mermaidBlock.classList.remove('is-loading'); - iframe.classList.remove('gt-invisible'); + iframe.classList.remove('tw-invisible'); }, 0); }); diff --git a/web_src/js/markup/tasklist.js b/web_src/js/markup/tasklist.js index ad1c6964a7..00076bce58 100644 --- a/web_src/js/markup/tasklist.js +++ b/web_src/js/markup/tasklist.js @@ -1,4 +1,4 @@ -import $ from 'jquery'; +import {POST} from '../modules/fetch.js'; const preventListener = (e) => e.preventDefault(); @@ -55,12 +55,11 @@ export function initMarkupTasklist() { const updateUrl = editContentZone.getAttribute('data-update-url'); const context = editContentZone.getAttribute('data-context'); - await $.post(updateUrl, { - ignore_attachments: true, - _csrf: window.config.csrfToken, - content: newContent, - context - }); + const requestBody = new FormData(); + requestBody.append('ignore_attachments', 'true'); + requestBody.append('content', newContent); + requestBody.append('context', context); + await POST(updateUrl, {data: requestBody}); rawContent.textContent = newContent; } catch (err) { diff --git a/web_src/js/modules/fetch.js b/web_src/js/modules/fetch.js index b3529d27fc..2191a8d4db 100644 --- a/web_src/js/modules/fetch.js +++ b/web_src/js/modules/fetch.js @@ -8,19 +8,17 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']); // fetch wrapper, use below method name functions and the `data` option to pass in data // which will automatically set an appropriate headers. For json content, only object // and array types are currently supported. -export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) { - let contentType; - if (!body) { - if (data instanceof FormData || data instanceof URLSearchParams) { - body = data; - } else if (isObject(data) || Array.isArray(data)) { - contentType = 'application/json'; - body = JSON.stringify(data); - } +export function request(url, {method = 'GET', data, headers = {}, ...other} = {}) { + let body, contentType; + if (data instanceof FormData || data instanceof URLSearchParams) { + body = data; + } else if (isObject(data) || Array.isArray(data)) { + contentType = 'application/json'; + body = JSON.stringify(data); } const headersMerged = new Headers({ - ...(!safeMethods.has(method.toUpperCase()) && {'x-csrf-token': csrfToken}), + ...(!safeMethods.has(method) && {'x-csrf-token': csrfToken}), ...(contentType && {'content-type': contentType}), }); @@ -31,8 +29,8 @@ export function request(url, {method = 'GET', headers = {}, data, body, ...other return fetch(url, { method, headers: headersMerged, - ...(body && {body}), ...other, + ...(body && {body}), }); } diff --git a/web_src/js/modules/fomantic.js b/web_src/js/modules/fomantic.js index 0c7a7ae641..d205c2b2ee 100644 --- a/web_src/js/modules/fomantic.js +++ b/web_src/js/modules/fomantic.js @@ -11,13 +11,11 @@ export const fomanticMobileScreen = window.matchMedia('only screen and (max-widt export function initGiteaFomantic() { // Silence fomantic's error logging when tabs are used without a target content element $.fn.tab.settings.silent = true; - // Disable the behavior of fomantic to toggle the checkbox when you press enter on a checkbox element. - $.fn.checkbox.settings.enableEnterKey = false; // By default, use "exact match" for full text search $.fn.dropdown.settings.fullTextSearch = 'exact'; // Do not use "cursor: pointer" for dropdown labels - $.fn.dropdown.settings.className.label += ' gt-cursor-default'; + $.fn.dropdown.settings.className.label += ' tw-cursor-default'; // Always use Gitea's SVG icons $.fn.dropdown.settings.templates.label = function(_value, text, preserveHTML, className) { const escape = $.fn.dropdown.settings.templates.escape; diff --git a/web_src/js/modules/fomantic/aria.md b/web_src/js/modules/fomantic/aria.md index a32d15f46f..5836a34506 100644 --- a/web_src/js/modules/fomantic/aria.md +++ b/web_src/js/modules/fomantic/aria.md @@ -2,10 +2,10 @@ This document is used as aria/accessibility(a11y) reference for future developers. -There are a lot of a11y problems in the Fomantic UI library. This `aria.js` is used -as a workaround to make the UI more accessible. +There are a lot of a11y problems in the Fomantic UI library. Files in +`web_src/js/modules/fomantic/` are used as a workaround to make the UI more accessible. -The `aria.js` is designed to avoid touching the official Fomantic UI library, +The aria-related code is designed to avoid touching the official Fomantic UI library, and to be as independent as possible, so it can be easily modified/removed in the future. To test the aria/accessibility with screen readers, developers can use the following steps: @@ -14,7 +14,7 @@ To test the aria/accessibility with screen readers, developers can use the follo * Press `Command + F5` to turn on VoiceOver. * Try to operate the UI with keyboard-only. * Use Tab/Shift+Tab to switch focus between elements. - * Arrow keys to navigate between menu/combobox items (only aria-active, not really focused). + * Arrow keys (Option+Up/Down) to navigate between menu/combobox items (only aria-active, not really focused). * Press Enter to trigger the aria-active element. * On Android, you can use TalkBack. * Go to Settings -> Accessibility -> TalkBack, turn it on. @@ -41,24 +41,19 @@ The ideal checkboxes should be: <label><input type="checkbox"> ... </label> ``` -However, related CSS styles aren't supported (not implemented) yet, so at the moment, -almost all the checkboxes are still using Fomantic UI checkbox. - -## Fomantic UI Checkbox +However, the templates still have the Fomantic-style HTML layout: ```html <div class="ui checkbox"> - <input type="checkbox"> <!-- class "hidden" will be added by $.checkbox() --> + <input type="checkbox"> <label>...</label> </div> ``` -Then the JS `$.checkbox()` should be called to make it work with keyboard and label-clicking, -then it works like the ideal checkboxes. - -There is still a problem: Fomantic UI checkbox is not friendly to screen readers, -so we add IDs to all the Fomantic UI checkboxes automatically by JS. -If the `label` part is empty, then the checkbox needs to get the `aria-label` attribute manually. +We call `initAriaCheckboxPatch` to link the `input` and `label` which makes clicking the +label etc. work. There is still a problem: These checkboxes are not friendly to screen readers, +so we add IDs to all the Fomantic UI checkboxes automatically by JS. If the `label` part is empty, +then the checkbox needs to get the `aria-label` attribute manually. # Fomantic Dropdown @@ -75,7 +70,7 @@ Fomantic Dropdown is designed to be used for many purposes: Fomantic Dropdown requires that the focus must be on its primary element. If the focus changes, it hides or panics. -At the moment, `aria.js` only tries to partially resolve the a11y problems for dropdowns with items. +At the moment, the aria-related code only tries to partially resolve the a11y problems for dropdowns with items. There are different solutions: diff --git a/web_src/js/modules/fomantic/checkbox.js b/web_src/js/modules/fomantic/checkbox.js index 08af1c2eb6..7f2b340296 100644 --- a/web_src/js/modules/fomantic/checkbox.js +++ b/web_src/js/modules/fomantic/checkbox.js @@ -1,38 +1,24 @@ -import $ from 'jquery'; import {generateAriaId} from './base.js'; -const ariaPatchKey = '_giteaAriaPatchCheckbox'; -const fomanticCheckboxFn = $.fn.checkbox; - -// use our own `$.fn.checkbox` to patch Fomantic's checkbox module export function initAriaCheckboxPatch() { - if ($.fn.checkbox === ariaCheckboxFn) throw new Error('initAriaCheckboxPatch could only be called once'); - $.fn.checkbox = ariaCheckboxFn; - ariaCheckboxFn.settings = fomanticCheckboxFn.settings; -} + // link the label and the input element so it's clickable and accessible + for (const el of document.querySelectorAll('.ui.checkbox')) { + if (el.hasAttribute('data-checkbox-patched')) continue; + const label = el.querySelector('label'); + const input = el.querySelector('input'); + if (!label || !input) continue; + const inputId = input.getAttribute('id'); + const labelFor = label.getAttribute('for'); -// the patched `$.fn.checkbox` checkbox function -// * it does the one-time attaching on the first call -function ariaCheckboxFn(...args) { - const ret = fomanticCheckboxFn.apply(this, args); - for (const el of this) { - if (el[ariaPatchKey]) continue; - attachInit(el); + if (inputId && !labelFor) { // missing "for" + label.setAttribute('for', inputId); + } else if (!inputId && !labelFor) { // missing both "id" and "for" + const id = generateAriaId(); + input.setAttribute('id', id); + label.setAttribute('for', id); + } else { + continue; + } + el.setAttribute('data-checkbox-patched', 'true'); } - return ret; -} - -function attachInit(el) { - // Fomantic UI checkbox needs to be something like: <div class="ui checkbox"><label /><input /></div> - // It doesn't work well with <label><input />...</label> - // To make it work with aria, the "id"/"for" attributes are necessary, so add them automatically if missing. - // In the future, refactor to use native checkbox directly, then this patch could be removed. - el[ariaPatchKey] = {}; // record that this element has been patched - const label = el.querySelector('label'); - const input = el.querySelector('input'); - if (!label || !input || input.getAttribute('id')) return; - - const id = generateAriaId(); - input.setAttribute('id', id); - label.setAttribute('for', id); } diff --git a/web_src/js/modules/fomantic/dropdown.js b/web_src/js/modules/fomantic/dropdown.js index c053256dd5..82e710860d 100644 --- a/web_src/js/modules/fomantic/dropdown.js +++ b/web_src/js/modules/fomantic/dropdown.js @@ -21,12 +21,11 @@ function ariaDropdownFn(...args) { // it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks. const needDelegate = (!args.length || typeof args[0] !== 'string'); for (const el of this) { - const $dropdown = $(el); if (!el[ariaPatchKey]) { - attachInit($dropdown); + attachInit(el); } if (needDelegate) { - delegateOne($dropdown); + delegateOne($(el)); } } return ret; @@ -38,19 +37,25 @@ function updateMenuItem(dropdown, item) { if (!item.id) item.id = generateAriaId(); item.setAttribute('role', dropdown[ariaPatchKey].listItemRole); item.setAttribute('tabindex', '-1'); - for (const a of item.querySelectorAll('a')) a.setAttribute('tabindex', '-1'); + for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1'); } - -// make the label item and its "delete icon" has correct aria attributes -function updateSelectionLabel($label) { +/** + * make the label item and its "delete icon" have correct aria attributes + * @param {HTMLElement} label + */ +function updateSelectionLabel(label) { // the "label" is like this: "<a|div class="ui label" data-value="1">the-label-name <i|svg class="delete icon"/></a>" - if (!$label.attr('id')) $label.attr('id', generateAriaId()); - $label.attr('tabindex', '-1'); - $label.find('.delete.icon').attr({ - 'aria-hidden': 'false', - 'aria-label': window.config.i18n.remove_label_str.replace('%s', $label.attr('data-value')), - 'role': 'button', - }); + if (!label.id) { + label.id = generateAriaId(); + } + label.tabIndex = -1; + + const deleteIcon = label.querySelector('.delete.icon'); + if (deleteIcon) { + deleteIcon.setAttribute('aria-hidden', 'false'); + deleteIcon.setAttribute('aria-label', window.config.i18n.remove_label_str.replace('%s', label.getAttribute('data-value'))); + deleteIcon.setAttribute('role', 'button'); + } } // delegate the dropdown's template functions and callback functions to add aria attributes. @@ -72,7 +77,9 @@ function delegateOne($dropdown) { dropdownTemplates.menu = function(response, fields, preserveHTML, className) { // when the dropdown menu items are loaded from AJAX requests, the items are created dynamically const menuItems = dropdownTemplatesMenuOld(response, fields, preserveHTML, className); - const $wrapper = $('<div>').append(menuItems); + const div = document.createElement('div'); + div.innerHTML = menuItems; + const $wrapper = $(div); const $items = $wrapper.find('> .item'); $items.each((_, item) => updateMenuItem($dropdown[0], item)); $dropdown[0][ariaPatchKey].deferredRefreshAriaActiveItem(); @@ -84,43 +91,44 @@ function delegateOne($dropdown) { const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate'); dropdownCall('setting', 'onLabelCreate', function(value, text) { const $label = dropdownOnLabelCreateOld.call(this, value, text); - updateSelectionLabel($label); + updateSelectionLabel($label[0]); return $label; }); } // for static dropdown elements (generated by server-side template), prepare them with necessary aria attributes -function attachStaticElements($dropdown, $focusable, $menu) { - const dropdown = $dropdown[0]; - +function attachStaticElements(dropdown, focusable, menu) { // prepare static dropdown menu list popup - if (!$menu.attr('id')) $menu.attr('id', generateAriaId()); - $menu.find('> .item').each((_, item) => updateMenuItem(dropdown, item)); + if (!menu.id) { + menu.id = generateAriaId(); + } + + $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item)); + // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash - $menu.attr('role', dropdown[ariaPatchKey].listPopupRole); + menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole); // prepare selection label items - $dropdown.find('.ui.label').each((_, label) => updateSelectionLabel($(label))); + for (const label of dropdown.querySelectorAll('.ui.label')) { + updateSelectionLabel(label); + } // make the primary element (focusable) aria-friendly - $focusable.attr({ - 'role': $focusable.attr('role') ?? dropdown[ariaPatchKey].focusableRole, - 'aria-haspopup': dropdown[ariaPatchKey].listPopupRole, - 'aria-controls': $menu.attr('id'), - 'aria-expanded': 'false', - }); + focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole); + focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole); + focusable.setAttribute('aria-controls', menu.id); + focusable.setAttribute('aria-expanded', 'false'); // use tooltip's content as aria-label if there is no aria-label - const tooltipContent = $dropdown.attr('data-tooltip-content'); - if (tooltipContent && !$dropdown.attr('aria-label')) { - $dropdown.attr('aria-label', tooltipContent); + const tooltipContent = dropdown.getAttribute('data-tooltip-content'); + if (tooltipContent && !dropdown.getAttribute('aria-label')) { + dropdown.setAttribute('aria-label', tooltipContent); } } -function attachInit($dropdown) { - const dropdown = $dropdown[0]; +function attachInit(dropdown) { dropdown[ariaPatchKey] = {}; - if ($dropdown.hasClass('custom')) return; + if (dropdown.classList.contains('custom')) return; // Dropdown has 2 different focusing behaviors // * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element. @@ -137,67 +145,69 @@ function attachInit($dropdown) { // TODO: multiple selection is only partially supported. Check and test them one by one in the future. - const $textSearch = $dropdown.find('input.search').eq(0); - const $focusable = $textSearch.length ? $textSearch : $dropdown; // the primary element for focus, see comment above - if (!$focusable.length) return; + const textSearch = dropdown.querySelector('input.search'); + const focusable = textSearch || dropdown; // the primary element for focus, see comment above + if (!focusable) return; // as a combobox, the input should not have autocomplete by default - if ($textSearch.length && !$textSearch.attr('autocomplete')) { - $textSearch.attr('autocomplete', 'off'); + if (textSearch && !textSearch.getAttribute('autocomplete')) { + textSearch.setAttribute('autocomplete', 'off'); } - let $menu = $dropdown.find('> .menu'); - if (!$menu.length) { + let menu = $(dropdown).find('> .menu')[0]; + if (!menu) { // some "multiple selection" dropdowns don't have a static menu element in HTML, we need to pre-create it to make it have correct aria attributes - $menu = $('<div class="menu"></div>').appendTo($dropdown); + menu = document.createElement('div'); + menu.classList.add('menu'); + dropdown.append(menu); } // There are 2 possible solutions about the role: combobox or menu. // The idea is that if there is an input, then it's a combobox, otherwise it's a menu. // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before. - const isComboBox = $dropdown.find('input').length > 0; + const isComboBox = dropdown.querySelectorAll('input').length > 0; dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu'; dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : ''; dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem'; - attachDomEvents($dropdown, $focusable, $menu); - attachStaticElements($dropdown, $focusable, $menu); + attachDomEvents(dropdown, focusable, menu); + attachStaticElements(dropdown, focusable, menu); } -function attachDomEvents($dropdown, $focusable, $menu) { - const dropdown = $dropdown[0]; +function attachDomEvents(dropdown, focusable, menu) { // when showing, it has class: ".animating.in" // when hiding, it has class: ".visible.animating.out" - const isMenuVisible = () => ($menu.hasClass('visible') && !$menu.hasClass('out')) || $menu.hasClass('in'); + const isMenuVisible = () => (menu.classList.contains('visible') && !menu.classList.contains('out')) || menu.classList.contains('in'); // update aria attributes according to current active/selected item const refreshAriaActiveItem = () => { const menuVisible = isMenuVisible(); - $focusable.attr('aria-expanded', menuVisible ? 'true' : 'false'); + focusable.setAttribute('aria-expanded', menuVisible ? 'true' : 'false'); // if there is an active item, use it (the user is navigating between items) // otherwise use the "selected" for combobox (for the last selected item) - const $active = $menu.find('> .item.active, > .item.selected'); + const active = $(menu).find('> .item.active, > .item.selected')[0]; + if (!active) return; // if the popup is visible and has an active/selected item, use its id as aria-activedescendant if (menuVisible) { - $focusable.attr('aria-activedescendant', $active.attr('id')); + focusable.setAttribute('aria-activedescendant', active.id); } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') { // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item - $focusable.removeAttr('aria-activedescendant'); - $active.removeClass('active').removeClass('selected'); + focusable.removeAttribute('aria-activedescendant'); + active.classList.remove('active', 'selected'); } }; - $dropdown.on('keydown', (e) => { + dropdown.addEventListener('keydown', (e) => { // here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler if (e.key === 'Enter') { - const dropdownCall = fomanticDropdownFn.bind($dropdown); + const dropdownCall = fomanticDropdownFn.bind($(dropdown)); let $item = dropdownCall('get item', dropdownCall('get value')); - if (!$item) $item = $menu.find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item + if (!$item) $item = $(menu).find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item // if the selected item is clickable, then trigger the click event. // we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click. - if ($item && ($item.is('a') || $item.hasClass('js-aria-clickable'))) $item[0].click(); + if ($item?.[0]?.matches('a, .js-aria-clickable')) $item[0].click(); } }); @@ -207,7 +217,7 @@ function attachDomEvents($dropdown, $focusable, $menu) { // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation. const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) }; dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem; - $dropdown.on('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); }); + dropdown.addEventListener('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); }); // if the dropdown has been opened by focus, do not trigger the next click event again. // otherwise the dropdown will be closed immediately, especially on Android with TalkBack diff --git a/web_src/js/modules/fomantic/modal.js b/web_src/js/modules/fomantic/modal.js index 7c9aade790..8b455cf4de 100644 --- a/web_src/js/modules/fomantic/modal.js +++ b/web_src/js/modules/fomantic/modal.js @@ -19,7 +19,9 @@ function ariaModalFn(...args) { // In such case, the "Enter" key will trigger the "cancel" button instead of "ok" button, then the dialog will be closed. // It breaks the user experience - the "Enter" key should confirm the dialog and submit the form. // So, all "cancel" buttons without "[type]" must be marked as "type=button". - $(el).find('form button.cancel:not([type])').attr('type', 'button'); + for (const button of el.querySelectorAll('form button.cancel:not([type])')) { + button.setAttribute('type', 'button'); + } } } return ret; diff --git a/web_src/js/modules/sortable.js b/web_src/js/modules/sortable.js index cfe7c3bf30..1c9adb6d72 100644 --- a/web_src/js/modules/sortable.js +++ b/web_src/js/modules/sortable.js @@ -1,4 +1,19 @@ -export async function createSortable(...args) { +export async function createSortable(el, opts = {}) { const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs'); - return new Sortable(...args); + + return new Sortable(el, { + animation: 150, + ghostClass: 'card-ghost', + onChoose: (e) => { + const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item; + handle.classList.add('tw-cursor-grabbing'); + opts.onChoose?.(e); + }, + onUnchoose: (e) => { + const handle = opts.handle ? e.item.querySelector(opts.handle) : e.item; + handle.classList.remove('tw-cursor-grabbing'); + opts.onUnchoose?.(e); + }, + ...opts, + }); } diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js index 27f371fd88..83b28e5745 100644 --- a/web_src/js/modules/tippy.js +++ b/web_src/js/modules/tippy.js @@ -1,12 +1,15 @@ import tippy, {followCursor} from 'tippy.js'; import {isDocumentFragmentOrElementNode} from '../utils/dom.js'; +import {formatDatetime} from '../utils/time.js'; const visibleInstances = new Set(); +const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`; export function createTippy(target, opts = {}) { // the callback functions should be destructured from opts, // because we should use our own wrapper functions to handle them, do not let the user override them - const {onHide, onShow, onDestroy, ...other} = opts; + const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts; + const instance = tippy(target, { appendTo: document.body, animation: false, @@ -33,18 +36,15 @@ export function createTippy(target, opts = {}) { visibleInstances.add(instance); return onShow?.(instance); }, - arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`, - role: 'menu', // HTML role attribute, only tooltips should use "tooltip" - theme: other.role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header" + arrow: arrow || (theme === 'bare' ? false : arrowSvg), + role: role || 'menu', // HTML role attribute + theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare" plugins: [followCursor], ...other, }); - // for popups where content refers to a DOM element, we use the 'tippy-target' class - // to initially hide the content, now we can remove it as the content has been removed - // from the DOM by tippy - if (other.content instanceof Element) { - other.content.classList.remove('tippy-target'); + if (role === 'menu') { + target.setAttribute('aria-haspopup', 'true'); } return instance; @@ -93,8 +93,15 @@ function attachTooltip(target, content = null) { } function switchTitleToTooltip(target) { - const title = target.getAttribute('title'); + let title = target.getAttribute('title'); if (title) { + // apply custom formatting to relative-time's tooltips + if (target.tagName.toLowerCase() === 'relative-time') { + const datetime = target.getAttribute('datetime'); + if (datetime) { + title = formatDatetime(new Date(datetime)); + } + } target.setAttribute('data-tooltip-content', title); target.setAttribute('aria-label', title); // keep the attribute, in case there are some other "[title]" selectors @@ -142,7 +149,7 @@ export function initGlobalTooltips() { const observerConnect = (observer) => observer.observe(document, { subtree: true, childList: true, - attributeFilter: ['data-tooltip-content', 'title'] + attributeFilter: ['data-tooltip-content', 'title'], }); const observer = new MutationObserver((mutationList, observer) => { const pending = observer.takeRecords(); diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js index fa075aed48..d12d203718 100644 --- a/web_src/js/modules/toast.js +++ b/web_src/js/modules/toast.js @@ -21,13 +21,12 @@ const levels = { }; // See https://github.com/apvarun/toastify-js#api for options -function showToast(message, level, {gravity, position, duration, ...other} = {}) { +function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) { const {icon, background, duration: levelDuration} = levels[level ?? 'info']; - const toast = Toastify({ text: ` <div class='toast-icon'>${svg(icon)}</div> - <div class='toast-body'>${htmlEscape(message)}</div> + <div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div> <button class='toast-close'>${svg('octicon-x')}</button> `, escapeMarkup: false, @@ -40,6 +39,7 @@ function showToast(message, level, {gravity, position, duration, ...other} = {}) toast.showToast(); toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); + return toast; } export function showInfoToast(message, opts) { diff --git a/web_src/js/standalone/swagger.js b/web_src/js/standalone/swagger.js index cb91089daf..00854ef5d7 100644 --- a/web_src/js/standalone/swagger.js +++ b/web_src/js/standalone/swagger.js @@ -21,11 +21,11 @@ window.addEventListener('load', async () => { docExpansion: 'none', defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete presets: [ - SwaggerUI.presets.apis + SwaggerUI.presets.apis, ], plugins: [ - SwaggerUI.plugins.DownloadUrl - ] + SwaggerUI.plugins.DownloadUrl, + ], }); window.ui = ui; diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 084256587c..913d26779f 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -67,6 +67,7 @@ import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethro import octiconSync from '../../public/assets/img/svg/octicon-sync.svg'; import octiconTable from '../../public/assets/img/svg/octicon-table.svg'; import octiconTag from '../../public/assets/img/svg/octicon-tag.svg'; +import octiconTrash from '../../public/assets/img/svg/octicon-trash.svg'; import octiconTriangleDown from '../../public/assets/img/svg/octicon-triangle-down.svg'; import octiconX from '../../public/assets/img/svg/octicon-x.svg'; import octiconXCircleFill from '../../public/assets/img/svg/octicon-x-circle-fill.svg'; @@ -139,6 +140,7 @@ const svgs = { 'octicon-sync': octiconSync, 'octicon-table': octiconTable, 'octicon-tag': octiconTag, + 'octicon-trash': octiconTrash, 'octicon-triangle-down': octiconTriangleDown, 'octicon-x': octiconX, 'octicon-x-circle-fill': octiconXCircleFill, @@ -187,7 +189,7 @@ export const SvgIcon = { name: {type: String, required: true}, size: {type: Number, default: 16}, className: {type: String, default: ''}, - symbolId: {type: String} + symbolId: {type: String}, }, render() { let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name); @@ -211,7 +213,7 @@ export const SvgIcon = { classes.push(...this.className.split(/\s+/).filter(Boolean)); } if (this.symbolId) { - classes.push('gt-hidden', 'svg-symbol-container'); + classes.push('tw-hidden', 'svg-symbol-container'); svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`; } // create VNode diff --git a/web_src/js/utils.js b/web_src/js/utils.js index c82e42d349..ce0fb66343 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -2,13 +2,14 @@ import {encode, decode} from 'uint8-to-base64'; // transform /path/to/file.ext to file.ext export function basename(path = '') { - return path ? path.replace(/^.*\//, '') : ''; + const lastSlashIndex = path.lastIndexOf('/'); + return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1); } // transform /path/to/file.ext to .ext export function extname(path = '') { - const [_, ext] = /.+(\.[^.]+)$/.exec(path) || []; - return ext || ''; + const lastPointIndex = path.lastIndexOf('.'); + return lastPointIndex < 0 ? '' : path.substring(lastPointIndex); } // test whether a variable is an object @@ -139,3 +140,5 @@ export function parseDom(text, contentType) { export function serializeXml(node) { return xmlSerializer.serializeToString(node); } + +export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js index 5d9c4ca45d..198f97c454 100644 --- a/web_src/js/utils/color.js +++ b/web_src/js/utils/color.js @@ -1,21 +1,33 @@ -// Check similar implementation in modules/util/color.go and keep synchronization -// Return R, G, B values defined in reletive luminance -function getLuminanceRGB(channel) { - const sRGB = channel / 255; - return (sRGB <= 0.03928) ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4; +import tinycolor from 'tinycolor2'; + +// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance +// Keep this in sync with modules/util/color.go +function getRelativeLuminance(color) { + const {r, g, b} = tinycolor(color).toRgb(); + return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; } -// Reference from: https://www.w3.org/WAI/GL/wiki/Relative_luminance -function getLuminance(r, g, b) { - const R = getLuminanceRGB(r); - const G = getLuminanceRGB(g); - const B = getLuminanceRGB(b); - return 0.2126 * R + 0.7152 * G + 0.0722 * B; +function useLightText(backgroundColor) { + return getRelativeLuminance(backgroundColor) < 0.453; } -// 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 -export function useLightTextOnBackground(r, g, b) { - return getLuminance(r, g, b) < 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 +export function contrastColor(backgroundColor) { + return useLightText(backgroundColor) ? '#fff' : '#000'; } + +function resolveColors(obj) { + const styles = window.getComputedStyle(document.documentElement); + const getColor = (name) => styles.getPropertyValue(name).trim(); + return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)])); +} + +export const chartJsColors = resolveColors({ + text: '--color-text', + border: '--color-secondary-alpha-60', + commits: '--color-primary-alpha-60', + additions: '--color-green', + deletions: '--color-red', +}); diff --git a/web_src/js/utils/color.test.js b/web_src/js/utils/color.test.js index e129109ef0..fee9afc776 100644 --- a/web_src/js/utils/color.test.js +++ b/web_src/js/utils/color.test.js @@ -1,21 +1,22 @@ -import {useLightTextOnBackground} from './color.js'; +import {contrastColor} from './color.js'; -test('useLightTextOnBackground', () => { - expect(useLightTextOnBackground(215, 58, 74)).toBe(true); - expect(useLightTextOnBackground(0, 117, 202)).toBe(true); - expect(useLightTextOnBackground(207, 211, 215)).toBe(false); - expect(useLightTextOnBackground(162, 238, 239)).toBe(false); - expect(useLightTextOnBackground(112, 87, 255)).toBe(true); - expect(useLightTextOnBackground(0, 134, 114)).toBe(true); - expect(useLightTextOnBackground(228, 230, 105)).toBe(false); - expect(useLightTextOnBackground(216, 118, 227)).toBe(true); - expect(useLightTextOnBackground(255, 255, 255)).toBe(false); - expect(useLightTextOnBackground(43, 134, 133)).toBe(true); - expect(useLightTextOnBackground(43, 135, 134)).toBe(true); - expect(useLightTextOnBackground(44, 135, 134)).toBe(true); - expect(useLightTextOnBackground(59, 182, 179)).toBe(true); - expect(useLightTextOnBackground(124, 114, 104)).toBe(true); - expect(useLightTextOnBackground(126, 113, 108)).toBe(true); - expect(useLightTextOnBackground(129, 112, 109)).toBe(true); - expect(useLightTextOnBackground(128, 112, 112)).toBe(true); +test('contrastColor', () => { + expect(contrastColor('#d73a4a')).toBe('#fff'); + expect(contrastColor('#0075ca')).toBe('#fff'); + expect(contrastColor('#cfd3d7')).toBe('#000'); + expect(contrastColor('#a2eeef')).toBe('#000'); + expect(contrastColor('#7057ff')).toBe('#fff'); + expect(contrastColor('#008672')).toBe('#fff'); + expect(contrastColor('#e4e669')).toBe('#000'); + expect(contrastColor('#d876e3')).toBe('#000'); + expect(contrastColor('#ffffff')).toBe('#000'); + expect(contrastColor('#2b8684')).toBe('#fff'); + expect(contrastColor('#2b8786')).toBe('#fff'); + expect(contrastColor('#2c8786')).toBe('#000'); + expect(contrastColor('#3bb6b3')).toBe('#000'); + expect(contrastColor('#7c7268')).toBe('#fff'); + expect(contrastColor('#7e716c')).toBe('#fff'); + expect(contrastColor('#81706d')).toBe('#fff'); + expect(contrastColor('#807070')).toBe('#fff'); + expect(contrastColor('#84b6eb')).toBe('#000'); }); diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js index 4dc55a518a..fb23a71725 100644 --- a/web_src/js/utils/dom.js +++ b/web_src/js/utils/dom.js @@ -22,11 +22,11 @@ function elementsCall(el, func, ...args) { */ function toggleShown(el, force) { if (force === true) { - el.classList.remove('gt-hidden'); + el.classList.remove('tw-hidden'); } else if (force === false) { - el.classList.add('gt-hidden'); + el.classList.add('tw-hidden'); } else if (force === undefined) { - el.classList.toggle('gt-hidden'); + el.classList.toggle('tw-hidden'); } else { throw new Error('invalid force argument'); } @@ -46,11 +46,29 @@ export function toggleElem(el, force) { export function isElemHidden(el) { const res = []; - elementsCall(el, (e) => res.push(e.classList.contains('gt-hidden'))); + elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden'))); if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`); return res[0]; } +function applyElemsCallback(elems, fn) { + if (fn) { + for (const el of elems) { + fn(el); + } + } + return elems; +} + +export function queryElemSiblings(el, selector = '*', fn) { + return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn); +} + +// it works like jQuery.children: only the direct children are selected +export function queryElemChildren(parent, selector = '*', fn) { + return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn); +} + export function onDomReady(cb) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', cb); @@ -187,7 +205,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) { textarea.removeEventListener('mousemove', onUserResize); textarea.removeEventListener('input', resizeToFit); textarea.form?.removeEventListener('reset', onFormReset); - } + }, }; } @@ -211,6 +229,7 @@ export function loadElem(el, src) { const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined'; export function submitEventSubmitter(e) { + e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter; } @@ -226,3 +245,51 @@ export function initSubmitEventPolyfill() { document.body.addEventListener('click', submitEventPolyfillListener); document.body.addEventListener('focus', submitEventPolyfillListener); } + +/** + * Check if an element is visible, equivalent to jQuery's `:visible` pseudo. + * Note: This function doesn't account for all possible visibility scenarios. + * @param {HTMLElement} element The element to check. + * @returns {boolean} True if the element is visible. + */ +export function isElemVisible(element) { + if (!element) return false; + + return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); +} + +// extract text and images from "paste" event +export function getPastedContent(e) { + const images = []; + for (const item of e.clipboardData?.items ?? []) { + if (item.type?.startsWith('image/')) { + images.push(item.getAsFile()); + } + } + const text = e.clipboardData?.getData?.('text') ?? ''; + return {text, images}; +} + +// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this +export function replaceTextareaSelection(textarea, text) { + const before = textarea.value.slice(0, textarea.selectionStart ?? undefined); + const after = textarea.value.slice(textarea.selectionEnd ?? undefined); + let success = true; + + textarea.contentEditable = 'true'; + try { + success = document.execCommand('insertText', false, text); + } catch { + success = false; + } + textarea.contentEditable = 'false'; + + if (success && !textarea.value.slice(0, textarea.selectionStart ?? undefined).endsWith(text)) { + success = false; + } + + if (!success) { + textarea.value = `${before}${text}${after}`; + textarea.dispatchEvent(new CustomEvent('change', {bubbles: true, cancelable: true})); + } +} diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js new file mode 100644 index 0000000000..ed5d98e35a --- /dev/null +++ b/web_src/js/utils/image.js @@ -0,0 +1,47 @@ +export async function pngChunks(blob) { + const uint8arr = new Uint8Array(await blob.arrayBuffer()); + const chunks = []; + if (uint8arr.length < 12) return chunks; + const view = new DataView(uint8arr.buffer); + if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; + + const decoder = new TextDecoder(); + let index = 8; + while (index < uint8arr.length) { + const len = view.getUint32(index); + chunks.push({ + name: decoder.decode(uint8arr.slice(index + 4, index + 8)), + data: uint8arr.slice(index + 8, index + 8 + len), + }); + index += len + 12; + } + + return chunks; +} + +// decode a image and try to obtain width and dppx. If will never throw but instead +// return default values. +export async function imageInfo(blob) { + let width = 0; // 0 means no width could be determined + let dppx = 1; // 1 dot per pixel for non-HiDPI screens + + if (blob.type === 'image/png') { // only png is supported currently + try { + for (const {name, data} of await pngChunks(blob)) { + const view = new DataView(data.buffer); + if (name === 'IHDR' && data?.length) { + // extract width from mandatory IHDR chunk + width = view.getUint32(0); + } else if (name === 'pHYs' && data?.length) { + // extract dppx from optional pHYs chunk, assuming pixels are square + const unit = view.getUint8(8); + if (unit === 1) { + dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx + } + } + } + } catch {} + } + + return {width, dppx}; +} diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js new file mode 100644 index 0000000000..ba4758250c --- /dev/null +++ b/web_src/js/utils/image.test.js @@ -0,0 +1,29 @@ +import {pngChunks, imageInfo} from './image.js'; + +const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg=='; +const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; +const pngEmpty = 'data:image/png;base64,'; + +async function dataUriToBlob(datauri) { + return await (await globalThis.fetch(datauri)).blob(); +} + +test('pngChunks', async () => { + expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([ + {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, + {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, + {name: 'IEND', data: new Uint8Array([])}, + ]); + expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([ + {name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])}, + {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])}, + {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])}, + ]); + expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]); +}); + +test('imageInfo', async () => { + expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); + expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2}); + expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1}); +}); diff --git a/web_src/js/utils/time.js b/web_src/js/utils/time.js new file mode 100644 index 0000000000..1848792c98 --- /dev/null +++ b/web_src/js/utils/time.js @@ -0,0 +1,67 @@ +import dayjs from 'dayjs'; +import {getCurrentLocale} from '../utils.js'; + +// Returns an array of millisecond-timestamps of start-of-week days (Sundays) +export function startDaysBetween(startDate, endDate) { + // Ensure the start date is a Sunday + while (startDate.getDay() !== 0) { + startDate.setDate(startDate.getDate() + 1); + } + + const start = dayjs(startDate); + const end = dayjs(endDate); + const startDays = []; + + let current = start; + while (current.isBefore(end)) { + startDays.push(current.valueOf()); + // we are adding 7 * 24 hours instead of 1 week because we don't want + // date library to use local time zone to calculate 1 week from now. + // local time zone is problematic because of daylight saving time (dst) + // used on some countries + current = current.add(7 * 24, 'hour'); + } + + return startDays; +} + +export function firstStartDateAfterDate(inputDate) { + if (!(inputDate instanceof Date)) { + throw new Error('Invalid date'); + } + const dayOfWeek = inputDate.getDay(); + const daysUntilSunday = 7 - dayOfWeek; + const resultDate = new Date(inputDate.getTime()); + resultDate.setDate(resultDate.getDate() + daysUntilSunday); + return resultDate.valueOf(); +} + +export function fillEmptyStartDaysWithZeroes(startDays, data) { + const result = {}; + + for (const startDay of startDays) { + result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0}; + } + + return Object.values(result); +} + +let dateFormat; + +// format a Date object to document's locale, but with 24h format from user's current locale because this +// option is a personal preference of the user, not something that the document's locale should dictate. +export function formatDatetime(date) { + if (!dateFormat) { + // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support + dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: 'numeric', + hour12: !Number.isInteger(Number(new Intl.DateTimeFormat([], {hour: 'numeric'}).format())), + minute: '2-digit', + timeZoneName: 'short', + }); + } + return dateFormat.format(date); +} diff --git a/web_src/js/utils/time.test.js b/web_src/js/utils/time.test.js new file mode 100644 index 0000000000..dd1114ce7f --- /dev/null +++ b/web_src/js/utils/time.test.js @@ -0,0 +1,15 @@ +import {startDaysBetween} from './time.js'; + +test('startDaysBetween', () => { + expect(startDaysBetween(new Date('2024-02-15'), new Date('2024-04-18'))).toEqual([ + 1708214400000, + 1708819200000, + 1709424000000, + 1710028800000, + 1710633600000, + 1711238400000, + 1711843200000, + 1712448000000, + 1713052800000, + ]); +}); diff --git a/web_src/js/utils/url.js b/web_src/js/utils/url.js index a40737ca6f..470ece31b0 100644 --- a/web_src/js/utils/url.js +++ b/web_src/js/utils/url.js @@ -1,3 +1,15 @@ export function pathEscapeSegments(s) { return s.split('/').map(encodeURIComponent).join('/'); } + +function stripSlash(url) { + return url.endsWith('/') ? url.slice(0, -1) : url; +} + +export function isUrl(url) { + try { + return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim(); + } catch { + return false; + } +} diff --git a/web_src/js/utils/url.test.js b/web_src/js/utils/url.test.js index 3dbedec94f..08c6373ffb 100644 --- a/web_src/js/utils/url.test.js +++ b/web_src/js/utils/url.test.js @@ -1,6 +1,13 @@ -import {pathEscapeSegments} from './url.js'; +import {pathEscapeSegments, isUrl} from './url.js'; test('pathEscapeSegments', () => { expect(pathEscapeSegments('a/b/c')).toEqual('a/b/c'); expect(pathEscapeSegments('a/b/ c')).toEqual('a/b/%20c'); }); + +test('isUrl', () => { + expect(isUrl('https://example.com')).toEqual(true); + expect(isUrl('https://example.com/')).toEqual(true); + expect(isUrl('https://example.com/index.html')).toEqual(true); + expect(isUrl('/index.html')).toEqual(false); +}); diff --git a/web_src/js/test/setup.js b/web_src/js/vitest.setup.js similarity index 100% rename from web_src/js/test/setup.js rename to web_src/js/vitest.setup.js diff --git a/web_src/js/webcomponents/README.md b/web_src/js/webcomponents/README.md index 0fde507310..45af58e1d2 100644 --- a/web_src/js/webcomponents/README.md +++ b/web_src/js/webcomponents/README.md @@ -6,7 +6,6 @@ https://developer.mozilla.org/en-US/docs/Web/Web_Components # Guidelines -* These components are loaded in `<head>` (before DOM body), - so they should have their own dependencies and should be very light, - then they won't affect the page loading time too much. -* If the component is not a public one, it's suggested to have its own `Gitea` or `gitea-` prefix to avoid conflicts. +* These components are loaded in `<head>` (before DOM body) in a separate entry point, they need to be lightweight to not affect the page loading time too much. +* Do not import `svg.js` into a web component because that file is currently not tree-shakeable, import svg files individually insteat. +* All our components must be added to `webpack.config.js` so they work correctly in Vue. diff --git a/web_src/js/webcomponents/absolute-date.js b/web_src/js/webcomponents/absolute-date.js new file mode 100644 index 0000000000..d2be455302 --- /dev/null +++ b/web_src/js/webcomponents/absolute-date.js @@ -0,0 +1,40 @@ +import {Temporal} from 'temporal-polyfill'; + +export function toAbsoluteLocaleDate(dateStr, lang, opts) { + return Temporal.PlainDate.from(dateStr).toLocaleString(lang ?? [], opts); +} + +window.customElements.define('absolute-date', class extends HTMLElement { + static observedAttributes = ['date', 'year', 'month', 'weekday', 'day']; + + update = () => { + const year = this.getAttribute('year') ?? ''; + const month = this.getAttribute('month') ?? ''; + const weekday = this.getAttribute('weekday') ?? ''; + const day = this.getAttribute('day') ?? ''; + const lang = this.closest('[lang]')?.getAttribute('lang') || + this.ownerDocument.documentElement.getAttribute('lang') || ''; + + // only use the first 10 characters, e.g. the `yyyy-mm-dd` part + const dateStr = this.getAttribute('date').substring(0, 10); + + if (!this.shadowRoot) this.attachShadow({mode: 'open'}); + this.shadowRoot.textContent = toAbsoluteLocaleDate(dateStr, lang, { + ...(year && {year}), + ...(month && {month}), + ...(weekday && {weekday}), + ...(day && {day}), + }); + }; + + attributeChangedCallback(_name, oldValue, newValue) { + if (!this.initialized || oldValue === newValue) return; + this.update(); + } + + connectedCallback() { + this.initialized = false; + this.update(); + this.initialized = true; + } +}); diff --git a/web_src/js/webcomponents/absolute-date.test.js b/web_src/js/webcomponents/absolute-date.test.js new file mode 100644 index 0000000000..ba04451b65 --- /dev/null +++ b/web_src/js/webcomponents/absolute-date.test.js @@ -0,0 +1,15 @@ +import {toAbsoluteLocaleDate} from './absolute-date.js'; + +test('toAbsoluteLocaleDate', () => { + expect(toAbsoluteLocaleDate('2024-03-15', 'en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + })).toEqual('March 15, 2024'); + + expect(toAbsoluteLocaleDate('2024-03-15', 'de-DE', { + year: 'numeric', + month: 'long', + day: 'numeric', + })).toEqual('15. März 2024'); +}); diff --git a/web_src/js/webcomponents/index.js b/web_src/js/webcomponents/index.js new file mode 100644 index 0000000000..7cec9da734 --- /dev/null +++ b/web_src/js/webcomponents/index.js @@ -0,0 +1,5 @@ +import './polyfills.js'; +import '@github/relative-time-element'; +import './origin-url.js'; +import './overflow-menu.js'; +import './absolute-date.js'; diff --git a/web_src/js/webcomponents/GiteaOriginUrl.js b/web_src/js/webcomponents/origin-url.js similarity index 71% rename from web_src/js/webcomponents/GiteaOriginUrl.js rename to web_src/js/webcomponents/origin-url.js index 5d71d95c60..09aa77f2c0 100644 --- a/web_src/js/webcomponents/GiteaOriginUrl.js +++ b/web_src/js/webcomponents/origin-url.js @@ -1,7 +1,8 @@ -// Convert an absolute or relative URL to an absolute URL with the current origin +// Convert an absolute or relative URL to an absolute URL with the current origin. It only +// processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. +// NOTE: Keep this function in sync with clone_script.tmpl export function toOriginUrl(urlStr) { try { - // only process absolute HTTP/HTTPS URL or relative URLs ('/xxx' or '//host/xxx') if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) { const {origin, protocol, hostname, port} = window.location; const url = new URL(urlStr, origin); @@ -14,7 +15,7 @@ export function toOriginUrl(urlStr) { return urlStr; } -window.customElements.define('gitea-origin-url', class extends HTMLElement { +window.customElements.define('origin-url', class extends HTMLElement { connectedCallback() { this.textContent = toOriginUrl(this.getAttribute('data-url')); } diff --git a/web_src/js/webcomponents/GiteaOriginUrl.test.js b/web_src/js/webcomponents/origin-url.test.js similarity index 94% rename from web_src/js/webcomponents/GiteaOriginUrl.test.js rename to web_src/js/webcomponents/origin-url.test.js index f0629842b8..3b2ab89f2a 100644 --- a/web_src/js/webcomponents/GiteaOriginUrl.test.js +++ b/web_src/js/webcomponents/origin-url.test.js @@ -1,4 +1,4 @@ -import {toOriginUrl} from './GiteaOriginUrl.js'; +import {toOriginUrl} from './origin-url.js'; test('toOriginUrl', () => { const oldLocation = window.location; diff --git a/web_src/js/webcomponents/overflow-menu.js b/web_src/js/webcomponents/overflow-menu.js new file mode 100644 index 0000000000..604fce7d4b --- /dev/null +++ b/web_src/js/webcomponents/overflow-menu.js @@ -0,0 +1,198 @@ +import {throttle} from 'throttle-debounce'; +import {createTippy} from '../modules/tippy.js'; +import {isDocumentFragmentOrElementNode} from '../utils/dom.js'; +import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg'; + +window.customElements.define('overflow-menu', class extends HTMLElement { + updateItems = throttle(100, () => { + if (!this.tippyContent) { + const div = document.createElement('div'); + div.classList.add('tippy-target'); + div.tabIndex = '-1'; // for initial focus, programmatic focus only + div.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + const items = this.tippyContent.querySelectorAll('[role="menuitem"]'); + if (e.shiftKey) { + if (document.activeElement === items[0]) { + e.preventDefault(); + items[items.length - 1].focus(); + } + } else { + if (document.activeElement === items[items.length - 1]) { + e.preventDefault(); + items[0].focus(); + } + } + } else if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + this.button._tippy.hide(); + this.button.focus(); + } else if (e.key === ' ' || e.code === 'Enter') { + if (document.activeElement?.matches('[role="menuitem"]')) { + e.preventDefault(); + e.stopPropagation(); + document.activeElement.click(); + } + } else if (e.key === 'ArrowDown') { + if (document.activeElement?.matches('.tippy-target')) { + e.preventDefault(); + e.stopPropagation(); + document.activeElement.querySelector('[role="menuitem"]:first-of-type').focus(); + } else if (document.activeElement?.matches('[role="menuitem"]')) { + e.preventDefault(); + e.stopPropagation(); + document.activeElement.nextElementSibling?.focus(); + } + } else if (e.key === 'ArrowUp') { + if (document.activeElement?.matches('.tippy-target')) { + e.preventDefault(); + e.stopPropagation(); + document.activeElement.querySelector('[role="menuitem"]:last-of-type').focus(); + } else if (document.activeElement?.matches('[role="menuitem"]')) { + e.preventDefault(); + e.stopPropagation(); + document.activeElement.previousElementSibling?.focus(); + } + } + }); + this.append(div); + this.tippyContent = div; + } + + // move items in tippy back into the menu items for subsequent measurement + for (const item of this.tippyItems || []) { + this.menuItemsEl.append(item); + } + + // measure which items are partially outside the element and move them into the button menu + this.tippyItems = []; + const menuRight = this.offsetLeft + this.offsetWidth; + const menuItems = this.menuItemsEl.querySelectorAll('.item'); + for (const item of menuItems) { + const itemRight = item.offsetLeft + item.offsetWidth; + if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button + this.tippyItems.push(item); + } + } + + // if there are no overflown items, remove any previously created button + if (!this.tippyItems?.length) { + const btn = this.querySelector('.overflow-menu-button'); + btn?._tippy?.destroy(); + btn?.remove(); + return; + } + + // remove aria role from items that moved from tippy to menu + for (const item of menuItems) { + if (!this.tippyItems.includes(item)) { + item.removeAttribute('role'); + } + } + + // move all items that overflow into tippy + for (const item of this.tippyItems) { + item.setAttribute('role', 'menuitem'); + this.tippyContent.append(item); + } + + // update existing tippy + if (this.button?._tippy) { + this.button._tippy.setContent(this.tippyContent); + return; + } + + // create button initially + const btn = document.createElement('button'); + btn.classList.add('overflow-menu-button', 'btn', 'tw-px-2', 'hover:tw-text-text-dark'); + btn.setAttribute('aria-label', window.config.i18n.more_items); + btn.innerHTML = octiconKebabHorizontal; + this.append(btn); + this.button = btn; + + createTippy(btn, { + trigger: 'click', + hideOnClick: true, + interactive: true, + placement: 'bottom-end', + role: 'menu', + content: this.tippyContent, + onShow: () => { // FIXME: onShown doesn't work (never be called) + setTimeout(() => { + this.tippyContent.focus(); + }, 0); + }, + }); + }); + + init() { + // for horizontal menus where fomantic boldens active items, prevent this bold text from + // enlarging the menu's active item replacing the text node with a div that renders a + // invisible pseudo-element that enlarges the box. + if (this.matches('.ui.secondary.pointing.menu, .ui.tabular.menu')) { + for (const item of this.querySelectorAll('.item')) { + for (const child of item.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + const text = child.textContent.trim(); // whitespace is insignificant inside flexbox + if (!text) continue; + const span = document.createElement('span'); + span.classList.add('resize-for-semibold'); + span.setAttribute('data-text', text); + span.textContent = text; + child.replaceWith(span); + } + } + } + } + + // ResizeObserver triggers on initial render, so we don't manually call `updateItems` here which + // also avoids a full-page FOUC in Firefox that happens when `updateItems` is called too soon. + this.resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const newWidth = entry.contentBoxSize[0].inlineSize; + if (newWidth !== this.lastWidth) { + requestAnimationFrame(() => { + this.updateItems(); + }); + this.lastWidth = newWidth; + } + } + }); + this.resizeObserver.observe(this); + } + + connectedCallback() { + this.setAttribute('role', 'navigation'); + + // check whether the mandatory `.overflow-menu-items` element is present initially which happens + // with Vue which renders differently than browsers. If it's not there, like in the case of browser + // template rendering, wait for its addition. + // The eslint rule is not sophisticated enough or aware of this problem, see + // https://github.com/43081j/eslint-plugin-wc/pull/130 + const menuItemsEl = this.querySelector('.overflow-menu-items'); // eslint-disable-line wc/no-child-traversal-in-connectedcallback + if (menuItemsEl) { + this.menuItemsEl = menuItemsEl; + this.init(); + } else { + this.mutationObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (!isDocumentFragmentOrElementNode(node)) continue; + if (node.classList.contains('overflow-menu-items')) { + this.menuItemsEl = node; + this.mutationObserver?.disconnect(); + this.init(); + } + } + } + }); + this.mutationObserver.observe(this, {childList: true}); + } + } + + disconnectedCallback() { + this.mutationObserver?.disconnect(); + this.resizeObserver?.disconnect(); + } +}); diff --git a/web_src/js/webcomponents/polyfill.js b/web_src/js/webcomponents/polyfills.js similarity index 98% rename from web_src/js/webcomponents/polyfill.js rename to web_src/js/webcomponents/polyfills.js index 88c7276881..38f50fa02f 100644 --- a/web_src/js/webcomponents/polyfill.js +++ b/web_src/js/webcomponents/polyfills.js @@ -9,7 +9,7 @@ try { return { format(value) { return ` ${value} ${options.unit}`; - } + }, }; } return intlNumberFormat(locales, options); diff --git a/web_src/js/webcomponents/webcomponents.js b/web_src/js/webcomponents/webcomponents.js deleted file mode 100644 index 916a588db6..0000000000 --- a/web_src/js/webcomponents/webcomponents.js +++ /dev/null @@ -1,5 +0,0 @@ -import '@webcomponents/custom-elements'; // polyfill for some browsers like PaleMoon -import './polyfill.js'; - -import '@github/relative-time-element'; -import './GiteaOriginUrl.js'; diff --git a/web_src/svg/gitea-bitbucket.svg b/web_src/svg/gitea-bitbucket.svg index d3b15a9dc6..ac490c944f 100644 --- a/web_src/svg/gitea-bitbucket.svg +++ b/web_src/svg/gitea-bitbucket.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 256 295" class="svg gitea-bitbucket" width="16" height="16" aria-hidden="true"><g fill="#205081"><path d="M128 0C57.732 0 .012 18.822.012 42.663c0 6.274 15.057 95.364 21.331 130.498 2.51 16.312 43.918 38.898 106.657 38.898 62.74 0 102.893-22.586 106.657-38.898 6.274-35.134 21.331-124.224 21.331-130.498C254.734 18.822 198.268 0 128 0zm0 183.199c-22.586 0-40.153-17.567-40.153-40.153s17.567-40.153 40.153-40.153 40.153 17.567 40.153 40.153c0 21.331-17.567 40.153-40.153 40.153zm0-127.988c-45.172 0-81.561-7.53-81.561-17.567 0-10.039 36.389-17.567 81.561-17.567 45.172 0 81.561 7.528 81.561 17.567 0 10.038-36.389 17.567-81.561 17.567z"/><path d="M220.608 207.04c-2.51 0-3.764 1.255-3.764 1.255s-31.37 25.096-87.835 25.096c-56.466 0-87.835-25.096-87.835-25.096s-2.51-1.255-3.765-1.255c-2.51 0-5.019 1.255-5.019 5.02v1.254c5.02 26.35 8.784 45.172 8.784 47.682 3.764 18.822 41.408 33.88 86.58 33.88s82.816-15.058 86.58-33.88c0-2.51 3.765-21.332 8.784-47.682v-1.255c1.255-2.51 0-5.019-2.51-5.019z"/><circle cx="128" cy="141.791" r="20.077"/></g></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.42 62.42"><defs><linearGradient id="a" x1="64.01" x2="32.99" y1="30.27" y2="54.48" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient></defs><g data-name="Layer 2"><path fill="#2684ff" d="M2 3.13a2 2 0 0 0-2 2.32l8.49 51.54a2.72 2.72 0 0 0 2.66 2.27h40.73a2 2 0 0 0 2-1.68l8.49-52.12a2 2 0 0 0-2-2.32Zm35.75 37.25h-13l-3.52-18.39H40.9Z"/><path fill="url(#a)" d="M59.67 25.12H40.9l-3.15 18.39h-13L9.4 61.73a2.71 2.71 0 0 0 1.75.66h40.74a2 2 0 0 0 2-1.68Z" transform="translate(0 -3.13)"/></g></svg> \ No newline at end of file diff --git a/web_src/svg/gitea-discord.svg b/web_src/svg/gitea-discord.svg index ea64a39f6e..4cadbc7f7e 100644 --- a/web_src/svg/gitea-discord.svg +++ b/web_src/svg/gitea-discord.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="0 0 256 293" class="svg gitea-discord" width="16" height="16" aria-hidden="true"><path fill="#7289DA" d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0zm-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732a2.022 2.022 0 0 1-.585-.438c-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.776 123.776 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238 0-8.923-6.582-16.237-14.92-16.237"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><path fill="#5865f2" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></svg> \ No newline at end of file diff --git a/web_src/svg/gitea-facebook.svg b/web_src/svg/gitea-facebook.svg index 8163e2a966..68cd20750a 100644 --- a/web_src/svg/gitea-facebook.svg +++ b/web_src/svg/gitea-facebook.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;fill-rule:evenodd;clip-rule:evenodd" viewBox="0 0 128 128" class="svg gitea-facebook" width="16" height="16" aria-hidden="true"><path fill="#395b97" d="M93.5 8.5c-1.452.802-3.118 1.302-5 1.5L10 88.5c-.198 1.882-.698 3.548-1.5 5a551.581 551.581 0 0 1-.5-56c2.5-17.167 12.333-27 29.5-29.5a551.581 551.581 0 0 1 56 .5Z" style="opacity:.995"/><path fill="#366098" d="M93.5 8.5c15.888 4.225 24.555 14.558 26 31a676.749 676.749 0 0 0-1.5 37l-35 35a32.438 32.438 0 0 0-.5 8 441.615 441.615 0 0 1-1-42h14a379.883 379.883 0 0 0 3-17h-17c-2.5-13.83 3.166-19.83 17-18v-16c-25.755-3.243-36.755 8.09-33 34h-14v17h14v42c-9.34.166-18.673 0-28-.5-15.451-1.953-25.118-10.453-29-25.5.802-1.452 1.302-3.118 1.5-5L88.5 10c1.882-.198 3.548-.698 5-1.5Z" style="opacity:.976"/><path fill="#346499" d="M119.5 39.5c.167 16.67 0 33.337-.5 50-3.622 20.245-15.788 30.245-36.5 30a32.438 32.438 0 0 1 .5-8l35-35c.169-12.507.669-24.84 1.5-37Z" style="opacity:.918"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 14222 14222"><g fill-rule="nonzero"><path fill="#1977f3" d="M14222 7111C14222 3184 11038 0 7111 0S0 3184 0 7111c0 3549 2600 6491 6000 7025V9167H4194V7111h1806V5544c0-1782 1062-2767 2686-2767 778 0 1592 139 1592 139v1750h-897c-883 0-1159 548-1159 1111v1334h1972l-315 2056H8222v4969c3400-533 6000-3475 6000-7025z"/><path fill="#fefefe" d="m9879 9167 315-2056H8222V5777c0-562 275-1111 1159-1111h897V2916s-814-139-1592-139c-1624 0-2686 984-2686 2767v1567H4194v2056h1806v4969c362 57 733 86 1111 86s749-30 1111-86V9167h1657z"/></g></svg> \ No newline at end of file diff --git a/web_src/svg/gitea-jetbrains.svg b/web_src/svg/gitea-jetbrains.svg new file mode 100644 index 0000000000..a7884c4289 --- /dev/null +++ b/web_src/svg/gitea-jetbrains.svg @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve"> +<g> + <g> + <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0.7898" y1="40.0893" x2="33.3172" y2="40.0893"> + <stop offset="0.2581" style="stop-color:#F97A12"/> + <stop offset="0.4591" style="stop-color:#B07B58"/> + <stop offset="0.7241" style="stop-color:#577BAE"/> + <stop offset="0.9105" style="stop-color:#1E7CE5"/> + <stop offset="1" style="stop-color:#087CFA"/> + </linearGradient> + <polygon style="fill:url(#SVGID_1_);" points="17.7,54.6 0.8,41.2 9.2,25.6 33.3,35 "/> + <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="25.7674" y1="24.88" x2="79.424" y2="54.57"> + <stop offset="0" style="stop-color:#F97A12"/> + <stop offset="7.179946e-002" style="stop-color:#CB7A3E"/> + <stop offset="0.1541" style="stop-color:#9E7B6A"/> + <stop offset="0.242" style="stop-color:#757B91"/> + <stop offset="0.3344" style="stop-color:#537BB1"/> + <stop offset="0.4324" style="stop-color:#387CCC"/> + <stop offset="0.5381" style="stop-color:#237CE0"/> + <stop offset="0.6552" style="stop-color:#147CEF"/> + <stop offset="0.7925" style="stop-color:#0B7CF7"/> + <stop offset="1" style="stop-color:#087CFA"/> + </linearGradient> + <polygon style="fill:url(#SVGID_2_);" points="70,18.7 68.7,59.2 41.8,70 25.6,59.6 49.3,35 38.9,12.3 48.2,1.1 "/> + <linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="63.2277" y1="42.9153" x2="48.2903" y2="-1.7191"> + <stop offset="0" style="stop-color:#FE315D"/> + <stop offset="7.840246e-002" style="stop-color:#CB417E"/> + <stop offset="0.1601" style="stop-color:#9E4E9B"/> + <stop offset="0.2474" style="stop-color:#755BB4"/> + <stop offset="0.3392" style="stop-color:#5365CA"/> + <stop offset="0.4365" style="stop-color:#386DDB"/> + <stop offset="0.5414" style="stop-color:#2374E9"/> + <stop offset="0.6576" style="stop-color:#1478F3"/> + <stop offset="0.794" style="stop-color:#0B7BF8"/> + <stop offset="1" style="stop-color:#087CFA"/> + </linearGradient> + <polygon style="fill:url(#SVGID_3_);" points="70,18.7 48.7,43.9 38.9,12.3 48.2,1.1 "/> + <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="10.7204" y1="16.473" x2="55.5237" y2="90.58"> + <stop offset="0" style="stop-color:#FE315D"/> + <stop offset="4.023279e-002" style="stop-color:#F63462"/> + <stop offset="0.1037" style="stop-color:#DF3A71"/> + <stop offset="0.1667" style="stop-color:#C24383"/> + <stop offset="0.2912" style="stop-color:#AD4A91"/> + <stop offset="0.5498" style="stop-color:#755BB4"/> + <stop offset="0.9175" style="stop-color:#1D76ED"/> + <stop offset="1" style="stop-color:#087CFA"/> + </linearGradient> + <polygon style="fill:url(#SVGID_4_);" points="33.7,58.1 5.6,68.3 10.1,52.5 16,33.1 0,27.7 10.1,0 32.1,2.7 53.7,27.4 "/> + </g> + <g> + <rect x="13.7" y="13.5" style="fill:#000000;" width="43.2" height="43.2"/> + <rect x="17.7" y="48.6" style="fill:#FFFFFF;" width="16.2" height="2.7"/> + <polygon style="fill:#FFFFFF;" points="29.4,22.4 29.4,19.1 20.4,19.1 20.4,22.4 23,22.4 23,33.7 20.4,33.7 20.4,37 29.4,37 + 29.4,33.7 26.9,33.7 26.9,22.4 "/> + <path style="fill:#FFFFFF;" d="M38,37.3c-1.4,0-2.6-0.3-3.5-0.8c-0.9-0.5-1.7-1.2-2.3-1.9l2.5-2.8c0.5,0.6,1,1,1.5,1.3 + c0.5,0.3,1.1,0.5,1.7,0.5c0.7,0,1.3-0.2,1.8-0.7c0.4-0.5,0.6-1.2,0.6-2.3V19.1h4v11.7c0,1.1-0.1,2-0.4,2.8c-0.3,0.8-0.7,1.4-1.3,2 + c-0.5,0.5-1.2,1-2,1.2C39.8,37.1,39,37.3,38,37.3"/> + </g> +</g> +</svg> \ No newline at end of file diff --git a/web_src/svg/gitea-microsoftonline.svg b/web_src/svg/gitea-microsoftonline.svg index 72ef94eabb..eb28296419 100644 --- a/web_src/svg/gitea-microsoftonline.svg +++ b/web_src/svg/gitea-microsoftonline.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2075 2499.8" class="svg gitea-microsoftonline" width="16" height="16" aria-hidden="true"><path fill="#eb3c00" d="M0 2016.6V496.8L1344.4 0 2075 233.7v2045.9l-730.6 220.3L0 2016.6l1344.4 161.8V409.2L467.6 613.8v1198.3z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48"><path fill="url(#a)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#b)" d="m20.084 3.026-.224.136a8.007 8.007 0 0 0-1.009.722l.648-.456H25L26 11l-5 5-5 3.475v4.008a8 8 0 0 0 3.857 6.844l5.264 3.186L14 40h-2.145l-3.998-2.42A8 8 0 0 1 4 30.737V17.26a8 8 0 0 1 3.86-6.846l12-7.258c.074-.045.149-.089.224-.131Z"/><path fill="url(#c)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#d)" d="M32 19v4.48a8 8 0 0 1-3.857 6.844l-12 7.264a8 8 0 0 1-8.008.16l11.722 7.096a8 8 0 0 0 8.286 0l12-7.264A8 8 0 0 0 44 30.736V27.5L43 26l-11-7Z"/><path fill="url(#e)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#f)" d="m40.14 10.415-12-7.258a8 8 0 0 0-8.042-.139l-.238.144A8 8 0 0 0 16 10.008v9.483l3.86-2.334a8 8 0 0 1 8.28 0l12 7.258A8 8 0 0 1 43.997 31c.003-.088.004-.175.004-.263V17.26a8 8 0 0 0-3.86-6.845Z"/><path fill="url(#g)" d="M4.004 30.998Z"/><path fill="url(#h)" d="M4.004 30.998Z"/><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="translate(17.4186 10.6383) rotate(110.528) scale(33.3657 58.1966)" gradientUnits="userSpaceOnUse"><stop offset=".064" stop-color="#AE7FE2"/><stop offset="1" stop-color="#0078D4"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientTransform="translate(10.4299 36.3511) rotate(-8.36717) scale(31.0503 20.5108)" gradientUnits="userSpaceOnUse"><stop offset=".134" stop-color="#D59DFF"/><stop offset="1" stop-color="#5E438F"/></radialGradient><radialGradient id="e" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><radialGradient id="g" cx="0" cy="0" r="1" gradientTransform="translate(41.0552 26.504) rotate(-165.772) scale(24.9228 41.9552)" gradientUnits="userSpaceOnUse"><stop offset=".058" stop-color="#50E6FF"/><stop offset="1" stop-color="#436DCD"/></radialGradient><linearGradient id="b" x1="17.512" x2="12.751" y1="37.868" y2="29.635" gradientUnits="userSpaceOnUse"><stop stop-color="#114A8B"/><stop offset="1" stop-color="#0078D4" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="40.357" x2="35.255" y1="25.377" y2="32.692" gradientUnits="userSpaceOnUse"><stop stop-color="#493474"/><stop offset="1" stop-color="#8C66BA" stop-opacity="0"/></linearGradient><linearGradient id="f" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient><linearGradient id="h" x1="16.976" x2="24.487" y1="3.057" y2="3.057" gradientUnits="userSpaceOnUse"><stop stop-color="#2D3F80"/><stop offset="1" stop-color="#436DCD" stop-opacity="0"/></linearGradient></defs></svg> \ No newline at end of file diff --git a/web_src/svg/gitea-twitter.svg b/web_src/svg/gitea-twitter.svg index 096b9add2b..f972d23f90 100644 --- a/web_src/svg/gitea-twitter.svg +++ b/web_src/svg/gitea-twitter.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="-89.009 -46.884 643.937 446.884" class="svg gitea-twitter" width="16" height="16" aria-hidden="true"><path fill="#1da1f2" fill-rule="nonzero" d="M154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052A205.304 205.304 0 0 0 492 47.346c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.42 202.42 0 0 1-64.098 24.511C395.903 12.276 369.679 0 340.641 0c-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504A100.739 100.739 0 0 0 20.57 69.24c0 35.034 17.82 65.961 44.92 84.055a100.172 100.172 0 0 1-45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101.036 101.036 0 0 1-26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224A204.9 204.9 0 0 1 0 354.634c44.674 28.645 97.72 45.359 154.734 45.359"/></svg> \ No newline at end of file +<svg viewBox="0 0 24 24"><path d="M14.095 10.316 22.286 1h-1.94L13.23 9.088 7.551 1H1l8.59 12.231L1 23h1.94l7.51-8.543 6 8.543H23l-8.905-12.684zm-2.658 3.022-.872-1.218L3.64 2.432h2.98l5.59 7.821.869 1.219 7.265 10.166h-2.982l-5.926-8.3z"/></svg> \ No newline at end of file diff --git a/web_src/svg/gitea-vscodium.svg b/web_src/svg/gitea-vscodium.svg new file mode 100644 index 0000000000..483676fe71 --- /dev/null +++ b/web_src/svg/gitea-vscodium.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="100%" height="100%" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" version="1.1" viewBox="0 0 16 16"><path fill-rule="nonzero" d="m10.2.2.5-.3c.3 0 .5.2.7.4l.2.8-.2 1-.8 2.4c-.3 1-.4 2 0 2.9a1046.4 1046.4 0 0 0 .8-2c.2 0 .4.1.4.3l-.3 1L9.2 13l3.1-2.9c.3-.2.7-.5.8-1a2 2 0 0 0-.3-1c-.2-.5-.5-.9-.6-1.4l.1-.7c.1-.1.3-.2.5-.1.2 0 .3.2.4.4.3.5.4 1.2.5 1.8l.6-1.2c0-.2.2-.4.4-.6l.4-.2c.2 0 .4.3.4.4v.6l-.8 1.6-1.4 1.8 1-.4c.2 0 .6.2.7.5 0 .2 0 .4-.2.5-.3.2-.6.2-1 .2-1 0-2.2.6-2.9 1.4L9.6 15c-.4.4-.9 1-1.4.8-.8-.1-.8-1.3-1-1.8 0-.3-.2-.6-.4-.7-.3-.2-.5-.3-.8-.3-.6-.1-1.2 0-1.8-.2l-.8-.4-.4-.7c-.3-.6-.3-1.2-.5-1.8A4 4 0 0 0 1 8l-.4-.4v-.4c.2-.2.5-.2.7 0 .5.2.5.8 1 1.1V6.2s.3-.1.4 0l.2.5L3 9c.4-.4.6-1 .5-1.5L3.4 7l.3-.2c.2 0 .3.2.4.3v.7c0 .6-.3 1.1-.4 1.7-.2.4-.3 1-.1 1.4.1.5.5.9.9 1 .5.3 1.1.4 1.7.4-.4-.6-.7-1.2-.7-2 0-.7.4-1.3.6-2C6.3 7 5.7 5.8 4.8 5l-1.5-.7c-.4-.2-.7-.7-.7-1.2.3-.1.7 0 1 .1L5 4.5l.6.1c.2-.3 0-.6-.2-.8-.3-.5-1-.6-1.3-1a.9.9 0 0 1-.2-.8c0-.2.3-.4.5-.4.4 0 .7.3.9.5.8.8 1.2 1.8 1.4 3 .2 1.2 0 2.5-.2 3.7 0 .3-.2.5-.1.8l.2.2c.2 0 .4 0 .5-.2.4-.3.8-.8.9-1.3l.1-1.2.1-.6.4-.2.3.3v.6c-.1.5-.2 1-.5 1.6a2 2 0 0 1-.6 1l-1 1c-.1.2-.2.6-.1.9 0 .2.2.4.4.5.4.2.8.2 1 0 .3-.1.5-.4.7-.6l.5-1.4.4-2.5C9.7 7 9.6 6 9 5.2c-.2-.4-.5-.7-1-1l-1-.8c-.2-.3-.4-.7-.3-1.2h.6c.4.1.7.4.9.8.2.4.4.8.9 1l-1-2c-.1-.3-.3-.5-.2-.8 0-.2.2-.4.4-.4s.4.1.5.3l.2.5 1 3.1a4 4 0 0 0 .4-2.3L10 1V.2Z"/></svg> \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 16afa0ff9c..fdf80a5313 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,6 +11,10 @@ import webpack from 'webpack'; import {fileURLToPath} from 'node:url'; import {readFileSync} from 'node:fs'; import {env} from 'node:process'; +import tailwindcss from 'tailwindcss'; +import tailwindConfig from './tailwind.config.js'; +import tailwindcssNesting from 'tailwindcss/nesting/index.js'; +import postcssNesting from 'postcss-nesting'; const {EsbuildPlugin} = EsBuildLoader; const {SourceMapDevToolPlugin, DefinePlugin} = webpack; @@ -39,6 +43,18 @@ if ('ENABLE_SOURCEMAP' in env) { sourceMaps = isProduction ? 'reduced' : 'true'; } +// define which web components we use for Vue to not interpret them as Vue components +const webComponents = new Set([ + // our own, in web_src/js/webcomponents + 'overflow-menu', + 'origin-url', + 'absolute-date', + // from dependencies + 'markdown-toolbar', + 'relative-time', + 'text-expander', +]); + const filterCssImport = (url, ...args) => { const cssFile = args[1] || args[0]; // resourcePath is 2nd argument for url and 3rd for import const importedFile = url.replace(/[?#].+/, '').toLowerCase(); @@ -68,7 +84,7 @@ export default { fileURLToPath(new URL('web_src/css/index.css', import.meta.url)), ], webcomponents: [ - fileURLToPath(new URL('web_src/js/webcomponents/webcomponents.js', import.meta.url)), + fileURLToPath(new URL('web_src/js/webcomponents/index.js', import.meta.url)), ], swagger: [ fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)), @@ -117,6 +133,11 @@ export default { test: /\.vue$/i, exclude: /node_modules/, loader: 'vue-loader', + options: { + compilerOptions: { + isCustomElement: (tag) => webComponents.has(tag), + }, + }, }, { test: /\.js$/i, @@ -143,6 +164,18 @@ export default { sourceMap: sourceMaps === 'true', url: {filter: filterCssImport}, import: {filter: filterCssImport}, + importLoaders: 1, + }, + }, + { + loader: 'postcss-loader', + options: { + postcssOptions: { + plugins: [ + tailwindcssNesting(postcssNesting({edition: '2024-02'})), + tailwindcss(tailwindConfig), + ], + }, }, }, ], @@ -157,21 +190,18 @@ export default { type: 'asset/resource', generator: { filename: 'fonts/[name].[contenthash:8][ext]', - } - }, - { - test: /\.png$/i, - type: 'asset/resource', - generator: { - filename: 'img/webpack/[name].[contenthash:8][ext]', - } + }, }, ], }, plugins: [ + new webpack.ProvidePlugin({ // for htmx extensions + htmx: 'htmx.org', + }), new DefinePlugin({ __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // https://github.com/vuejs/vue-cli/pull/7443 }), new VueLoaderPlugin(), new MiniCssExtractPlugin({ @@ -205,10 +235,10 @@ export default { }, override: { 'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33 - 'htmx.org@1.9.10': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause" + 'idiomorph@0.3.0': {licenseName: 'BSD-2-Clause'}, // https://github.com/bigskysoftware/idiomorph/pull/37 }, emitError: true, - allow: '(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)', + allow: '(Apache-2.0 OR 0BSD OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)', }) : new AddAssetPlugin('licenses.txt', `Licenses are disabled during development`), ], performance: {