Merge branch 'main' into add-file-tree-to-file-view-page

This commit is contained in:
Kerwin Bryant 2024-12-23 10:56:23 +08:00 committed by GitHub
commit 07fb84f830
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
329 changed files with 5412 additions and 5413 deletions

999
.eslintrc.cjs Normal file
View File

@ -0,0 +1,999 @@
const restrictedSyntax = ['WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'];
module.exports = {
root: true,
reportUnusedDisableDirectives: true,
ignorePatterns: [
'/web_src/js/vendor',
'/web_src/fomantic',
'/public/assets/js',
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
project: true,
extraFileExtensions: ['.vue'],
parser: '@typescript-eslint/parser', // for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
},
settings: {
'import-x/extensions': ['.js', '.ts'],
'import-x/parsers': {
'@typescript-eslint/parser': ['.js', '.ts'],
},
'import-x/resolver': {
typescript: true,
},
},
plugins: [
'@eslint-community/eslint-plugin-eslint-comments',
'@stylistic/eslint-plugin-js',
'@typescript-eslint/eslint-plugin',
'eslint-plugin-array-func',
'eslint-plugin-github',
'eslint-plugin-import-x',
'eslint-plugin-no-jquery',
'eslint-plugin-no-use-extend-native',
'eslint-plugin-regexp',
'eslint-plugin-sonarjs',
'eslint-plugin-unicorn',
'eslint-plugin-vitest',
'eslint-plugin-vitest-globals',
'eslint-plugin-wc',
],
env: {
es2024: true,
node: true,
},
overrides: [
{
files: ['web_src/**/*'],
globals: {
__webpack_public_path__: true,
process: false, // https://github.com/webpack/webpack/issues/15833
},
},
{
files: ['web_src/**/*', 'docs/**/*'],
env: {
browser: true,
node: false,
},
},
{
files: ['*.config.*'],
rules: {
'import-x/no-unused-modules': [0],
},
},
{
files: ['**/*.d.ts'],
rules: {
'import-x/no-unused-modules': [0],
'@typescript-eslint/consistent-type-definitions': [0],
'@typescript-eslint/consistent-type-imports': [0],
},
},
{
files: ['web_src/js/types.ts'],
rules: {
'import-x/no-unused-modules': [0],
},
},
{
files: ['**/*.test.*', 'web_src/js/test/setup.ts'],
env: {
'vitest-globals/env': true,
},
rules: {
'vitest/consistent-test-filename': [0],
'vitest/consistent-test-it': [0],
'vitest/expect-expect': [0],
'vitest/max-expects': [0],
'vitest/max-nested-describe': [0],
'vitest/no-alias-methods': [0],
'vitest/no-commented-out-tests': [0],
'vitest/no-conditional-expect': [0],
'vitest/no-conditional-in-test': [0],
'vitest/no-conditional-tests': [0],
'vitest/no-disabled-tests': [0],
'vitest/no-done-callback': [0],
'vitest/no-duplicate-hooks': [0],
'vitest/no-focused-tests': [0],
'vitest/no-hooks': [0],
'vitest/no-identical-title': [2],
'vitest/no-interpolation-in-snapshots': [0],
'vitest/no-large-snapshots': [0],
'vitest/no-mocks-import': [0],
'vitest/no-restricted-matchers': [0],
'vitest/no-restricted-vi-methods': [0],
'vitest/no-standalone-expect': [0],
'vitest/no-test-prefixes': [0],
'vitest/no-test-return-statement': [0],
'vitest/prefer-called-with': [0],
'vitest/prefer-comparison-matcher': [0],
'vitest/prefer-each': [0],
'vitest/prefer-equality-matcher': [0],
'vitest/prefer-expect-resolves': [0],
'vitest/prefer-hooks-in-order': [0],
'vitest/prefer-hooks-on-top': [2],
'vitest/prefer-lowercase-title': [0],
'vitest/prefer-mock-promise-shorthand': [0],
'vitest/prefer-snapshot-hint': [0],
'vitest/prefer-spy-on': [0],
'vitest/prefer-strict-equal': [0],
'vitest/prefer-to-be': [0],
'vitest/prefer-to-be-falsy': [0],
'vitest/prefer-to-be-object': [0],
'vitest/prefer-to-be-truthy': [0],
'vitest/prefer-to-contain': [0],
'vitest/prefer-to-have-length': [0],
'vitest/prefer-todo': [0],
'vitest/require-hook': [0],
'vitest/require-to-throw-message': [0],
'vitest/require-top-level-describe': [0],
'vitest/valid-describe-callback': [2],
'vitest/valid-expect': [2],
'vitest/valid-title': [2],
},
},
{
files: ['web_src/js/modules/fetch.ts', 'web_src/js/standalone/**/*'],
rules: {
'no-restricted-syntax': [2, ...restrictedSyntax],
},
},
{
files: ['**/*.vue'],
plugins: [
'eslint-plugin-vue',
'eslint-plugin-vue-scoped-css',
],
extends: [
'plugin:vue/vue3-recommended',
'plugin:vue-scoped-css/vue3-recommended',
],
rules: {
'vue/attributes-order': [0],
'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}],
'vue/max-attributes-per-line': [0],
'vue/singleline-html-element-content-newline': [0],
},
},
{
files: ['tests/e2e/**'],
plugins: [
'eslint-plugin-playwright'
],
extends: [
'plugin:playwright/recommended',
],
},
],
rules: {
'@eslint-community/eslint-comments/disable-enable-pair': [2],
'@eslint-community/eslint-comments/no-aggregating-enable': [2],
'@eslint-community/eslint-comments/no-duplicate-disable': [2],
'@eslint-community/eslint-comments/no-restricted-disable': [0],
'@eslint-community/eslint-comments/no-unlimited-disable': [2],
'@eslint-community/eslint-comments/no-unused-disable': [2],
'@eslint-community/eslint-comments/no-unused-enable': [2],
'@eslint-community/eslint-comments/no-use': [0],
'@eslint-community/eslint-comments/require-description': [0],
'@stylistic/js/array-bracket-newline': [0],
'@stylistic/js/array-bracket-spacing': [2, 'never'],
'@stylistic/js/array-element-newline': [0],
'@stylistic/js/arrow-parens': [2, 'always'],
'@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, 'always-multiline'],
'@stylistic/js/comma-spacing': [2, {before: false, after: true}],
'@stylistic/js/comma-style': [2, 'last'],
'@stylistic/js/computed-property-spacing': [2, 'never'],
'@stylistic/js/dot-location': [2, 'property'],
'@stylistic/js/eol-last': [2],
'@stylistic/js/function-call-argument-newline': [0],
'@stylistic/js/function-call-spacing': [2, 'never'],
'@stylistic/js/function-paren-newline': [0],
'@stylistic/js/generator-star-spacing': [0],
'@stylistic/js/implicit-arrow-linebreak': [0],
'@stylistic/js/indent': [2, 2, {ignoreComments: true, SwitchCase: 1}],
'@stylistic/js/key-spacing': [2],
'@stylistic/js/keyword-spacing': [2],
'@stylistic/js/line-comment-position': [0],
'@stylistic/js/linebreak-style': [2, 'unix'],
'@stylistic/js/lines-around-comment': [0],
'@stylistic/js/lines-between-class-members': [0],
'@stylistic/js/max-len': [0],
'@stylistic/js/max-statements-per-line': [0],
'@stylistic/js/multiline-comment-style': [0],
'@stylistic/js/multiline-ternary': [0],
'@stylistic/js/new-parens': [2],
'@stylistic/js/newline-per-chained-call': [0],
'@stylistic/js/no-confusing-arrow': [0],
'@stylistic/js/no-extra-parens': [0],
'@stylistic/js/no-extra-semi': [2],
'@stylistic/js/no-floating-decimal': [0],
'@stylistic/js/no-mixed-operators': [0],
'@stylistic/js/no-mixed-spaces-and-tabs': [2],
'@stylistic/js/no-multi-spaces': [2, {ignoreEOLComments: true, exceptions: {Property: true}}],
'@stylistic/js/no-multiple-empty-lines': [2, {max: 1, maxEOF: 0, maxBOF: 0}],
'@stylistic/js/no-tabs': [2],
'@stylistic/js/no-trailing-spaces': [2],
'@stylistic/js/no-whitespace-before-property': [2],
'@stylistic/js/nonblock-statement-body-position': [2],
'@stylistic/js/object-curly-newline': [0],
'@stylistic/js/object-curly-spacing': [2, 'never'],
'@stylistic/js/object-property-newline': [0],
'@stylistic/js/one-var-declaration-per-line': [0],
'@stylistic/js/operator-linebreak': [2, 'after'],
'@stylistic/js/padded-blocks': [2, 'never'],
'@stylistic/js/padding-line-between-statements': [0],
'@stylistic/js/quote-props': [0],
'@stylistic/js/quotes': [2, 'single', {avoidEscape: true, allowTemplateLiterals: true}],
'@stylistic/js/rest-spread-spacing': [2, 'never'],
'@stylistic/js/semi': [2, 'always', {omitLastInOneLineBlock: true}],
'@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': [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],
'@stylistic/js/spaced-comment': [2, 'always'],
'@stylistic/js/switch-colon-spacing': [2],
'@stylistic/js/template-curly-spacing': [2, 'never'],
'@stylistic/js/template-tag-spacing': [2, 'never'],
'@stylistic/js/wrap-iife': [2, 'inside'],
'@stylistic/js/wrap-regex': [0],
'@stylistic/js/yield-star-spacing': [2, 'after'],
'@typescript-eslint/adjacent-overload-signatures': [0],
'@typescript-eslint/array-type': [0],
'@typescript-eslint/await-thenable': [2],
'@typescript-eslint/ban-ts-comment': [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}],
'@typescript-eslint/ban-tslint-comment': [0],
'@typescript-eslint/class-literal-property-style': [0],
'@typescript-eslint/class-methods-use-this': [0],
'@typescript-eslint/consistent-generic-constructors': [0],
'@typescript-eslint/consistent-indexed-object-style': [0],
'@typescript-eslint/consistent-return': [0],
'@typescript-eslint/consistent-type-assertions': [2, {assertionStyle: 'as', objectLiteralTypeAssertions: 'allow'}],
'@typescript-eslint/consistent-type-definitions': [2, 'type'],
'@typescript-eslint/consistent-type-exports': [2, {fixMixedExportsWithInlineTypeSpecifier: false}],
'@typescript-eslint/consistent-type-imports': [2, {prefer: 'type-imports', fixStyle: 'separate-type-imports', disallowTypeAnnotations: true}],
'@typescript-eslint/default-param-last': [0],
'@typescript-eslint/dot-notation': [0],
'@typescript-eslint/explicit-function-return-type': [0],
'@typescript-eslint/explicit-member-accessibility': [0],
'@typescript-eslint/explicit-module-boundary-types': [0],
'@typescript-eslint/init-declarations': [0],
'@typescript-eslint/max-params': [0],
'@typescript-eslint/member-ordering': [0],
'@typescript-eslint/method-signature-style': [0],
'@typescript-eslint/naming-convention': [0],
'@typescript-eslint/no-array-constructor': [2],
'@typescript-eslint/no-array-delete': [2],
'@typescript-eslint/no-base-to-string': [0],
'@typescript-eslint/no-confusing-non-null-assertion': [2],
'@typescript-eslint/no-confusing-void-expression': [0],
'@typescript-eslint/no-deprecated': [2],
'@typescript-eslint/no-dupe-class-members': [0],
'@typescript-eslint/no-duplicate-enum-values': [2],
'@typescript-eslint/no-duplicate-type-constituents': [2, {ignoreUnions: true}],
'@typescript-eslint/no-dynamic-delete': [0],
'@typescript-eslint/no-empty-function': [0],
'@typescript-eslint/no-empty-interface': [0],
'@typescript-eslint/no-empty-object-type': [2],
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/no-extra-non-null-assertion': [2],
'@typescript-eslint/no-extraneous-class': [0],
'@typescript-eslint/no-floating-promises': [0],
'@typescript-eslint/no-for-in-array': [2],
'@typescript-eslint/no-implied-eval': [2],
'@typescript-eslint/no-import-type-side-effects': [0], // dupe with consistent-type-imports
'@typescript-eslint/no-inferrable-types': [0],
'@typescript-eslint/no-invalid-this': [0],
'@typescript-eslint/no-invalid-void-type': [0],
'@typescript-eslint/no-loop-func': [0],
'@typescript-eslint/no-loss-of-precision': [0],
'@typescript-eslint/no-magic-numbers': [0],
'@typescript-eslint/no-meaningless-void-operator': [0],
'@typescript-eslint/no-misused-new': [2],
'@typescript-eslint/no-misused-promises': [2, {checksVoidReturn: {attributes: false, arguments: false}}],
'@typescript-eslint/no-mixed-enums': [0],
'@typescript-eslint/no-namespace': [2],
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0],
'@typescript-eslint/no-non-null-asserted-optional-chain': [2],
'@typescript-eslint/no-non-null-assertion': [0],
'@typescript-eslint/no-redeclare': [0],
'@typescript-eslint/no-redundant-type-constituents': [2],
'@typescript-eslint/no-require-imports': [2],
'@typescript-eslint/no-restricted-imports': [0],
'@typescript-eslint/no-restricted-types': [0],
'@typescript-eslint/no-shadow': [0],
'@typescript-eslint/no-this-alias': [0], // handled by unicorn/no-this-assignment
'@typescript-eslint/no-unnecessary-boolean-literal-compare': [0],
'@typescript-eslint/no-unnecessary-condition': [0],
'@typescript-eslint/no-unnecessary-qualifier': [0],
'@typescript-eslint/no-unnecessary-template-expression': [0],
'@typescript-eslint/no-unnecessary-type-arguments': [0],
'@typescript-eslint/no-unnecessary-type-assertion': [2],
'@typescript-eslint/no-unnecessary-type-constraint': [2],
'@typescript-eslint/no-unsafe-argument': [0],
'@typescript-eslint/no-unsafe-assignment': [0],
'@typescript-eslint/no-unsafe-call': [0],
'@typescript-eslint/no-unsafe-declaration-merging': [2],
'@typescript-eslint/no-unsafe-enum-comparison': [2],
'@typescript-eslint/no-unsafe-function-type': [2],
'@typescript-eslint/no-unsafe-member-access': [0],
'@typescript-eslint/no-unsafe-return': [0],
'@typescript-eslint/no-unsafe-unary-minus': [2],
'@typescript-eslint/no-unused-expressions': [0],
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}],
'@typescript-eslint/no-use-before-define': [0],
'@typescript-eslint/no-useless-constructor': [0],
'@typescript-eslint/no-useless-empty-export': [0],
'@typescript-eslint/no-wrapper-object-types': [2],
'@typescript-eslint/non-nullable-type-assertion-style': [0],
'@typescript-eslint/only-throw-error': [2],
'@typescript-eslint/parameter-properties': [0],
'@typescript-eslint/prefer-as-const': [2],
'@typescript-eslint/prefer-destructuring': [0],
'@typescript-eslint/prefer-enum-initializers': [0],
'@typescript-eslint/prefer-find': [2],
'@typescript-eslint/prefer-for-of': [2],
'@typescript-eslint/prefer-function-type': [2],
'@typescript-eslint/prefer-includes': [2],
'@typescript-eslint/prefer-literal-enum-member': [0],
'@typescript-eslint/prefer-namespace-keyword': [0],
'@typescript-eslint/prefer-nullish-coalescing': [0],
'@typescript-eslint/prefer-optional-chain': [2, {requireNullish: true}],
'@typescript-eslint/prefer-promise-reject-errors': [0],
'@typescript-eslint/prefer-readonly': [0],
'@typescript-eslint/prefer-readonly-parameter-types': [0],
'@typescript-eslint/prefer-reduce-type-parameter': [0],
'@typescript-eslint/prefer-regexp-exec': [0],
'@typescript-eslint/prefer-return-this-type': [0],
'@typescript-eslint/prefer-string-starts-ends-with': [2, {allowSingleElementEquality: 'always'}],
'@typescript-eslint/promise-function-async': [0],
'@typescript-eslint/require-array-sort-compare': [0],
'@typescript-eslint/require-await': [0],
'@typescript-eslint/restrict-plus-operands': [2],
'@typescript-eslint/restrict-template-expressions': [0],
'@typescript-eslint/return-await': [0],
'@typescript-eslint/strict-boolean-expressions': [0],
'@typescript-eslint/switch-exhaustiveness-check': [0],
'@typescript-eslint/triple-slash-reference': [2],
'@typescript-eslint/typedef': [0],
'@typescript-eslint/unbound-method': [0], // too many false-positives
'@typescript-eslint/unified-signatures': [2],
'accessor-pairs': [2],
'array-callback-return': [2, {checkForEach: true}],
'array-func/avoid-reverse': [2],
'array-func/from-map': [2],
'array-func/no-unnecessary-this-arg': [2],
'array-func/prefer-array-from': [2],
'array-func/prefer-flat-map': [0], // handled by unicorn/prefer-array-flat-map
'array-func/prefer-flat': [0], // handled by unicorn/prefer-array-flat
'arrow-body-style': [0],
'block-scoped-var': [2],
'camelcase': [0],
'capitalized-comments': [0],
'class-methods-use-this': [0],
'complexity': [0],
'consistent-return': [0],
'consistent-this': [0],
'constructor-super': [2],
'curly': [0],
'default-case-last': [2],
'default-case': [0],
'default-param-last': [0],
'dot-notation': [0],
'eqeqeq': [2],
'for-direction': [2],
'func-name-matching': [2],
'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],
'id-length': [0],
'id-match': [0],
'import-x/consistent-type-specifier-style': [0],
'import-x/default': [0],
'import-x/dynamic-import-chunkname': [0],
'import-x/export': [2],
'import-x/exports-last': [0],
'import-x/extensions': [2, 'always', {ignorePackages: true}],
'import-x/first': [2],
'import-x/group-exports': [0],
'import-x/max-dependencies': [0],
'import-x/named': [2],
'import-x/namespace': [0],
'import-x/newline-after-import': [0],
'import-x/no-absolute-path': [0],
'import-x/no-amd': [2],
'import-x/no-anonymous-default-export': [0],
'import-x/no-commonjs': [2],
'import-x/no-cycle': [2, {ignoreExternal: true, maxDepth: 1}],
'import-x/no-default-export': [0],
'import-x/no-deprecated': [0],
'import-x/no-dynamic-require': [0],
'import-x/no-empty-named-blocks': [2],
'import-x/no-extraneous-dependencies': [2],
'import-x/no-import-module-exports': [0],
'import-x/no-internal-modules': [0],
'import-x/no-mutable-exports': [0],
'import-x/no-named-as-default-member': [0],
'import-x/no-named-as-default': [0],
'import-x/no-named-default': [0],
'import-x/no-named-export': [0],
'import-x/no-namespace': [0],
'import-x/no-nodejs-modules': [0],
'import-x/no-relative-packages': [0],
'import-x/no-relative-parent-imports': [0],
'import-x/no-restricted-paths': [0],
'import-x/no-self-import': [2],
'import-x/no-unassigned-import': [0],
'import-x/no-unresolved': [2, {commonjs: true, ignore: ['\\?.+$']}],
'import-x/no-unused-modules': [2, {unusedExports: true}],
'import-x/no-useless-path-segments': [2, {commonjs: true}],
'import-x/no-webpack-loader-syntax': [2],
'import-x/order': [0],
'import-x/prefer-default-export': [0],
'import-x/unambiguous': [0],
'init-declarations': [0],
'line-comment-position': [0],
'logical-assignment-operators': [0],
'max-classes-per-file': [0],
'max-depth': [0],
'max-lines-per-function': [0],
'max-lines': [0],
'max-nested-callbacks': [0],
'max-params': [0],
'max-statements': [0],
'multiline-comment-style': [2, 'separate-lines'],
'new-cap': [0],
'no-alert': [0],
'no-array-constructor': [0], // handled by @typescript-eslint/no-array-constructor
'no-async-promise-executor': [0],
'no-await-in-loop': [0],
'no-bitwise': [0],
'no-buffer-constructor': [0],
'no-caller': [2],
'no-case-declarations': [2],
'no-class-assign': [2],
'no-compare-neg-zero': [2],
'no-cond-assign': [2, 'except-parens'],
'no-console': [1, {allow: ['debug', 'info', 'warn', 'error']}],
'no-const-assign': [2],
'no-constant-binary-expression': [2],
'no-constant-condition': [0],
'no-constructor-return': [2],
'no-continue': [0],
'no-control-regex': [0],
'no-debugger': [1],
'no-delete-var': [2],
'no-div-regex': [0],
'no-dupe-args': [2],
'no-dupe-class-members': [2],
'no-dupe-else-if': [2],
'no-dupe-keys': [2],
'no-duplicate-case': [2],
'no-duplicate-imports': [0],
'no-else-return': [2],
'no-empty-character-class': [2],
'no-empty-function': [0],
'no-empty-pattern': [2],
'no-empty-static-block': [2],
'no-empty': [2, {allowEmptyCatch: true}],
'no-eq-null': [2],
'no-eval': [2],
'no-ex-assign': [2],
'no-extend-native': [2],
'no-extra-bind': [2],
'no-extra-boolean-cast': [2],
'no-extra-label': [0],
'no-fallthrough': [2],
'no-func-assign': [2],
'no-global-assign': [2],
'no-implicit-coercion': [2],
'no-implicit-globals': [0],
'no-implied-eval': [0], // handled by @typescript-eslint/no-implied-eval
'no-import-assign': [2],
'no-inline-comments': [0],
'no-inner-declarations': [2],
'no-invalid-regexp': [2],
'no-invalid-this': [0],
'no-irregular-whitespace': [2],
'no-iterator': [2],
'no-jquery/no-ajax-events': [2],
'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': [2],
'no-jquery/no-attr': [2],
'no-jquery/no-bind': [2],
'no-jquery/no-box-model': [2],
'no-jquery/no-browser': [2],
'no-jquery/no-camel-case': [2],
'no-jquery/no-class-state': [2],
'no-jquery/no-class': [0],
'no-jquery/no-clone': [2],
'no-jquery/no-closest': [0],
'no-jquery/no-constructor-attributes': [2],
'no-jquery/no-contains': [2],
'no-jquery/no-context-prop': [2],
'no-jquery/no-css': [2],
'no-jquery/no-data': [0],
'no-jquery/no-deferred': [2],
'no-jquery/no-delegate': [2],
'no-jquery/no-done-fail': [2],
'no-jquery/no-each-collection': [0],
'no-jquery/no-each-util': [0],
'no-jquery/no-each': [0],
'no-jquery/no-error-shorthand': [2],
'no-jquery/no-error': [2],
'no-jquery/no-escape-selector': [2],
'no-jquery/no-event-shorthand': [2],
'no-jquery/no-extend': [2],
'no-jquery/no-fade': [2],
'no-jquery/no-filter': [0],
'no-jquery/no-find-collection': [0],
'no-jquery/no-find-util': [2],
'no-jquery/no-find': [0],
'no-jquery/no-fx-interval': [2],
'no-jquery/no-fx': [2],
'no-jquery/no-global-eval': [2],
'no-jquery/no-global-selector': [0],
'no-jquery/no-grep': [2],
'no-jquery/no-has': [2],
'no-jquery/no-hold-ready': [2],
'no-jquery/no-html': [0],
'no-jquery/no-in-array': [2],
'no-jquery/no-is-array': [2],
'no-jquery/no-is-empty-object': [2],
'no-jquery/no-is-function': [2],
'no-jquery/no-is-numeric': [2],
'no-jquery/no-is-plain-object': [2],
'no-jquery/no-is-window': [2],
'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': [2],
'no-jquery/no-merge': [2],
'no-jquery/no-node-name': [2],
'no-jquery/no-noop': [2],
'no-jquery/no-now': [2],
'no-jquery/no-on-ready': [2],
'no-jquery/no-other-methods': [0],
'no-jquery/no-other-utils': [2],
'no-jquery/no-param': [2],
'no-jquery/no-parent': [0],
'no-jquery/no-parents': [2],
'no-jquery/no-parse-html-literal': [2],
'no-jquery/no-parse-html': [2],
'no-jquery/no-parse-json': [2],
'no-jquery/no-parse-xml': [2],
'no-jquery/no-prop': [2],
'no-jquery/no-proxy': [2],
'no-jquery/no-ready-shorthand': [2],
'no-jquery/no-ready': [2],
'no-jquery/no-selector-prop': [2],
'no-jquery/no-serialize': [2],
'no-jquery/no-size': [2],
'no-jquery/no-sizzle': [2],
'no-jquery/no-slide': [2],
'no-jquery/no-sub': [2],
'no-jquery/no-support': [2],
'no-jquery/no-text': [2],
'no-jquery/no-trigger': [0],
'no-jquery/no-trim': [2],
'no-jquery/no-type': [2],
'no-jquery/no-unique': [2],
'no-jquery/no-unload-shorthand': [2],
'no-jquery/no-val': [0],
'no-jquery/no-visibility': [2],
'no-jquery/no-when': [2],
'no-jquery/no-wrap': [2],
'no-jquery/variable-pattern': [2],
'no-label-var': [2],
'no-labels': [0], // handled by no-restricted-syntax
'no-lone-blocks': [2],
'no-lonely-if': [0],
'no-loop-func': [0],
'no-loss-of-precision': [2],
'no-magic-numbers': [0],
'no-misleading-character-class': [2],
'no-multi-assign': [0],
'no-multi-str': [2],
'no-negated-condition': [0],
'no-nested-ternary': [0],
'no-new-func': [2],
'no-new-native-nonconstructor': [2],
'no-new-object': [2],
'no-new-symbol': [2],
'no-new-wrappers': [2],
'no-new': [0],
'no-nonoctal-decimal-escape': [2],
'no-obj-calls': [2],
'no-octal-escape': [2],
'no-octal': [2],
'no-param-reassign': [0],
'no-plusplus': [0],
'no-promise-executor-return': [0],
'no-proto': [2],
'no-prototype-builtins': [2],
'no-redeclare': [0], // must be disabled for typescript overloads
'no-regex-spaces': [2],
'no-restricted-exports': [0],
'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'],
'no-restricted-imports': [0],
'no-restricted-syntax': [2, ...restrictedSyntax, {selector: 'CallExpression[callee.name="fetch"]', message: 'use modules/fetch.ts instead'}],
'no-return-assign': [0],
'no-script-url': [2],
'no-self-assign': [2, {props: true}],
'no-self-compare': [2],
'no-sequences': [2],
'no-setter-return': [2],
'no-shadow-restricted-names': [2],
'no-shadow': [0],
'no-sparse-arrays': [2],
'no-template-curly-in-string': [2],
'no-ternary': [0],
'no-this-before-super': [2],
'no-throw-literal': [2],
'no-undef-init': [2],
'no-undef': [0],
'no-undefined': [0],
'no-underscore-dangle': [0],
'no-unexpected-multiline': [2],
'no-unmodified-loop-condition': [2],
'no-unneeded-ternary': [2],
'no-unreachable-loop': [2],
'no-unreachable': [2],
'no-unsafe-finally': [2],
'no-unsafe-negation': [2],
'no-unused-expressions': [2],
'no-unused-labels': [2],
'no-unused-private-class-members': [2],
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
'no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true}],
'no-use-extend-native/no-use-extend-native': [2],
'no-useless-backreference': [2],
'no-useless-call': [2],
'no-useless-catch': [2],
'no-useless-computed-key': [2],
'no-useless-concat': [2],
'no-useless-constructor': [2],
'no-useless-escape': [2],
'no-useless-rename': [2],
'no-useless-return': [2],
'no-var': [2],
'no-void': [2],
'no-warning-comments': [0],
'no-with': [0], // handled by no-restricted-syntax
'object-shorthand': [2, 'always'],
'one-var-declaration-per-line': [0],
'one-var': [0],
'operator-assignment': [2, 'always'],
'operator-linebreak': [2, 'after'],
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
'prefer-destructuring': [0],
'prefer-exponentiation-operator': [2],
'prefer-named-capture-group': [0],
'prefer-numeric-literals': [2],
'prefer-object-has-own': [2],
'prefer-object-spread': [2],
'prefer-promise-reject-errors': [2, {allowEmptyReject: false}],
'prefer-regex-literals': [2],
'prefer-rest-params': [2],
'prefer-spread': [2],
'prefer-template': [2],
'radix': [2, 'as-needed'],
'regexp/confusing-quantifier': [2],
'regexp/control-character-escape': [2],
'regexp/hexadecimal-escape': [0],
'regexp/letter-case': [0],
'regexp/match-any': [2],
'regexp/negation': [2],
'regexp/no-contradiction-with-assertion': [0],
'regexp/no-control-character': [0],
'regexp/no-dupe-characters-character-class': [2],
'regexp/no-dupe-disjunctions': [2],
'regexp/no-empty-alternative': [2],
'regexp/no-empty-capturing-group': [2],
'regexp/no-empty-character-class': [0],
'regexp/no-empty-group': [2],
'regexp/no-empty-lookarounds-assertion': [2],
'regexp/no-empty-string-literal': [2],
'regexp/no-escape-backspace': [2],
'regexp/no-extra-lookaround-assertions': [0],
'regexp/no-invalid-regexp': [2],
'regexp/no-invisible-character': [2],
'regexp/no-lazy-ends': [2],
'regexp/no-legacy-features': [2],
'regexp/no-misleading-capturing-group': [0],
'regexp/no-misleading-unicode-character': [0],
'regexp/no-missing-g-flag': [2],
'regexp/no-non-standard-flag': [2],
'regexp/no-obscure-range': [2],
'regexp/no-octal': [2],
'regexp/no-optional-assertion': [2],
'regexp/no-potentially-useless-backreference': [2],
'regexp/no-standalone-backslash': [2],
'regexp/no-super-linear-backtracking': [0],
'regexp/no-super-linear-move': [0],
'regexp/no-trivially-nested-assertion': [2],
'regexp/no-trivially-nested-quantifier': [2],
'regexp/no-unused-capturing-group': [0],
'regexp/no-useless-assertions': [2],
'regexp/no-useless-backreference': [2],
'regexp/no-useless-character-class': [2],
'regexp/no-useless-dollar-replacements': [2],
'regexp/no-useless-escape': [2],
'regexp/no-useless-flag': [2],
'regexp/no-useless-lazy': [2],
'regexp/no-useless-non-capturing-group': [2],
'regexp/no-useless-quantifier': [2],
'regexp/no-useless-range': [2],
'regexp/no-useless-set-operand': [2],
'regexp/no-useless-string-literal': [2],
'regexp/no-useless-two-nums-quantifier': [2],
'regexp/no-zero-quantifier': [2],
'regexp/optimal-lookaround-quantifier': [2],
'regexp/optimal-quantifier-concatenation': [0],
'regexp/prefer-character-class': [0],
'regexp/prefer-d': [0],
'regexp/prefer-escape-replacement-dollar-char': [0],
'regexp/prefer-lookaround': [0],
'regexp/prefer-named-backreference': [0],
'regexp/prefer-named-capture-group': [0],
'regexp/prefer-named-replacement': [0],
'regexp/prefer-plus-quantifier': [2],
'regexp/prefer-predefined-assertion': [2],
'regexp/prefer-quantifier': [0],
'regexp/prefer-question-quantifier': [2],
'regexp/prefer-range': [2],
'regexp/prefer-regexp-exec': [2],
'regexp/prefer-regexp-test': [2],
'regexp/prefer-result-array-groups': [0],
'regexp/prefer-set-operation': [2],
'regexp/prefer-star-quantifier': [2],
'regexp/prefer-unicode-codepoint-escapes': [2],
'regexp/prefer-w': [0],
'regexp/require-unicode-regexp': [0],
'regexp/simplify-set-operations': [2],
'regexp/sort-alternatives': [0],
'regexp/sort-character-class-elements': [0],
'regexp/sort-flags': [0],
'regexp/strict': [2],
'regexp/unicode-escape': [0],
'regexp/use-ignore-case': [0],
'require-atomic-updates': [0],
'require-await': [0], // handled by @typescript-eslint/require-await
'require-unicode-regexp': [0],
'require-yield': [2],
'sonarjs/cognitive-complexity': [0],
'sonarjs/elseif-without-else': [0],
'sonarjs/max-switch-cases': [0],
'sonarjs/no-all-duplicated-branches': [2],
'sonarjs/no-collapsible-if': [0],
'sonarjs/no-collection-size-mischeck': [2],
'sonarjs/no-duplicate-string': [0],
'sonarjs/no-duplicated-branches': [0],
'sonarjs/no-element-overwrite': [2],
'sonarjs/no-empty-collection': [2],
'sonarjs/no-extra-arguments': [2],
'sonarjs/no-gratuitous-expressions': [2],
'sonarjs/no-identical-conditions': [2],
'sonarjs/no-identical-expressions': [2],
'sonarjs/no-identical-functions': [2, 5],
'sonarjs/no-ignored-return': [2],
'sonarjs/no-inverted-boolean-check': [2],
'sonarjs/no-nested-switch': [0],
'sonarjs/no-nested-template-literals': [0],
'sonarjs/no-one-iteration-loop': [2],
'sonarjs/no-redundant-boolean': [2],
'sonarjs/no-redundant-jump': [2],
'sonarjs/no-same-line-conditional': [2],
'sonarjs/no-small-switch': [0],
'sonarjs/no-unused-collection': [2],
'sonarjs/no-use-of-empty-return-value': [2],
'sonarjs/no-useless-catch': [2],
'sonarjs/non-existent-operator': [2],
'sonarjs/prefer-immediate-return': [0],
'sonarjs/prefer-object-literal': [0],
'sonarjs/prefer-single-boolean-return': [0],
'sonarjs/prefer-while': [2],
'sort-imports': [0],
'sort-keys': [0],
'sort-vars': [0],
'strict': [0],
'symbol-description': [2],
'unicode-bom': [2, 'never'],
'unicorn/better-regex': [0],
'unicorn/catch-error-name': [0],
'unicorn/consistent-destructuring': [2],
'unicorn/consistent-empty-array-spread': [2],
'unicorn/consistent-existence-index-check': [0],
'unicorn/consistent-function-scoping': [0],
'unicorn/custom-error-definition': [0],
'unicorn/empty-brace-spaces': [2],
'unicorn/error-message': [0],
'unicorn/escape-case': [0],
'unicorn/expiring-todo-comments': [0],
'unicorn/explicit-length-check': [0],
'unicorn/filename-case': [0],
'unicorn/import-index': [0],
'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],
'unicorn/no-for-loop': [0],
'unicorn/no-hex-escape': [0],
'unicorn/no-instanceof-array': [0],
'unicorn/no-invalid-fetch-options': [2],
'unicorn/no-invalid-remove-event-listener': [2],
'unicorn/no-keyword-prefix': [0],
'unicorn/no-length-as-slice-end': [2],
'unicorn/no-lonely-if': [2],
'unicorn/no-magic-array-flat-depth': [0],
'unicorn/no-negated-condition': [0],
'unicorn/no-negation-in-equality-check': [2],
'unicorn/no-nested-ternary': [0],
'unicorn/no-new-array': [0],
'unicorn/no-new-buffer': [0],
'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],
'unicorn/no-typeof-undefined': [2],
'unicorn/no-unnecessary-await': [2],
'unicorn/no-unnecessary-polyfills': [2],
'unicorn/no-unreadable-array-destructuring': [0],
'unicorn/no-unreadable-iife': [2],
'unicorn/no-unused-properties': [2],
'unicorn/no-useless-fallback-in-spread': [2],
'unicorn/no-useless-length-check': [2],
'unicorn/no-useless-promise-resolve-reject': [2],
'unicorn/no-useless-spread': [2],
'unicorn/no-useless-switch-case': [2],
'unicorn/no-useless-undefined': [0],
'unicorn/no-zero-fractions': [2],
'unicorn/number-literal-case': [0],
'unicorn/numeric-separators-style': [0],
'unicorn/prefer-add-event-listener': [2],
'unicorn/prefer-array-find': [2],
'unicorn/prefer-array-flat-map': [2],
'unicorn/prefer-array-flat': [2],
'unicorn/prefer-array-index-of': [2],
'unicorn/prefer-array-some': [2],
'unicorn/prefer-at': [0],
'unicorn/prefer-blob-reading-methods': [2],
'unicorn/prefer-code-point': [0],
'unicorn/prefer-date-now': [2],
'unicorn/prefer-default-parameters': [0],
'unicorn/prefer-dom-node-append': [2],
'unicorn/prefer-dom-node-dataset': [0],
'unicorn/prefer-dom-node-remove': [2],
'unicorn/prefer-dom-node-text-content': [2],
'unicorn/prefer-event-target': [2],
'unicorn/prefer-export-from': [0],
'unicorn/prefer-global-this': [0],
'unicorn/prefer-includes': [2],
'unicorn/prefer-json-parse-buffer': [0],
'unicorn/prefer-keyboard-event-key': [2],
'unicorn/prefer-logical-operator-over-ternary': [2],
'unicorn/prefer-math-min-max': [2],
'unicorn/prefer-math-trunc': [2],
'unicorn/prefer-modern-dom-apis': [0],
'unicorn/prefer-modern-math-apis': [2],
'unicorn/prefer-module': [2],
'unicorn/prefer-native-coercion-functions': [2],
'unicorn/prefer-negative-index': [2],
'unicorn/prefer-node-protocol': [2],
'unicorn/prefer-number-properties': [0],
'unicorn/prefer-object-from-entries': [2],
'unicorn/prefer-object-has-own': [0],
'unicorn/prefer-optional-catch-binding': [2],
'unicorn/prefer-prototype-methods': [0],
'unicorn/prefer-query-selector': [2],
'unicorn/prefer-reflect-apply': [0],
'unicorn/prefer-regexp-test': [2],
'unicorn/prefer-set-has': [0],
'unicorn/prefer-set-size': [2],
'unicorn/prefer-spread': [0],
'unicorn/prefer-string-raw': [0],
'unicorn/prefer-string-replace-all': [0],
'unicorn/prefer-string-slice': [0],
'unicorn/prefer-string-starts-ends-with': [2],
'unicorn/prefer-string-trim-start-end': [2],
'unicorn/prefer-structured-clone': [2],
'unicorn/prefer-switch': [0],
'unicorn/prefer-ternary': [0],
'unicorn/prefer-text-content': [2],
'unicorn/prefer-top-level-await': [0],
'unicorn/prefer-type-error': [0],
'unicorn/prevent-abbreviations': [0],
'unicorn/relative-url-style': [2],
'unicorn/require-array-join-separator': [2],
'unicorn/require-number-to-fixed-digits-argument': [2],
'unicorn/require-post-message-target-origin': [0],
'unicorn/string-content': [0],
'unicorn/switch-case-braces': [0],
'unicorn/template-indent': [2],
'unicorn/text-encoding-identifier-case': [0],
'unicorn/throw-new-error': [2],
'use-isnan': [2],
'valid-typeof': [2, {requireStringLiterals: true}],
'vars-on-top': [0],
'wc/attach-shadow-constructor': [2],
'wc/define-tag-after-class-definition': [0],
'wc/expose-class-on-global': [0],
'wc/file-name-matches-element': [2],
'wc/guard-define-call': [0],
'wc/guard-super-call': [2],
'wc/max-elements-per-file': [0],
'wc/no-child-traversal-in-attributechangedcallback': [2],
'wc/no-child-traversal-in-connectedcallback': [2],
'wc/no-closed-shadow-root': [2],
'wc/no-constructor-attributes': [2],
'wc/no-constructor-params': [2],
'wc/no-constructor': [2],
'wc/no-customized-built-in-elements': [2],
'wc/no-exports-with-element': [0],
'wc/no-invalid-element-name': [2],
'wc/no-invalid-extends': [2],
'wc/no-method-prefixed-with-on': [2],
'wc/no-self-class': [2],
'wc/no-typos': [2],
'wc/require-listener-teardown': [2],
'wc/tag-name-matches-class': [2],
'yoda': [2, 'never'],
},
};

View File

@ -1,967 +0,0 @@
root: true
reportUnusedDisableDirectives: true
ignorePatterns:
- /web_src/js/vendor
- /web_src/fomantic
- /public/assets/js
parser: "@typescript-eslint/parser"
parserOptions:
sourceType: module
ecmaVersion: latest
project: true
extraFileExtensions: [".vue"]
parser: "@typescript-eslint/parser" # for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
settings:
import-x/extensions: [".js", ".ts"]
import-x/parsers:
"@typescript-eslint/parser": [".js", ".ts"]
import-x/resolver:
typescript: true
plugins:
- "@eslint-community/eslint-plugin-eslint-comments"
- "@stylistic/eslint-plugin-js"
- "@typescript-eslint/eslint-plugin"
- eslint-plugin-array-func
- eslint-plugin-github
- eslint-plugin-import-x
- eslint-plugin-no-jquery
- eslint-plugin-no-use-extend-native
- eslint-plugin-regexp
- eslint-plugin-sonarjs
- eslint-plugin-unicorn
- eslint-plugin-vitest
- eslint-plugin-vitest-globals
- eslint-plugin-wc
env:
es2024: true
node: true
overrides:
- files: ["web_src/**/*"]
globals:
__webpack_public_path__: true
process: false # https://github.com/webpack/webpack/issues/15833
- files: ["web_src/**/*", "docs/**/*"]
env:
browser: true
node: false
- files: ["web_src/**/*worker.*"]
env:
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: ["*.config.*"]
rules:
import-x/no-unused-modules: [0]
- files: ["**/*.d.ts"]
rules:
import-x/no-unused-modules: [0]
"@typescript-eslint/consistent-type-definitions": [0]
"@typescript-eslint/consistent-type-imports": [0]
- files: ["web_src/js/types.ts"]
rules:
import-x/no-unused-modules: [0]
- files: ["**/*.test.*", "web_src/js/test/setup.ts"]
env:
vitest-globals/env: true
rules:
vitest/consistent-test-filename: [0]
vitest/consistent-test-it: [0]
vitest/expect-expect: [0]
vitest/max-expects: [0]
vitest/max-nested-describe: [0]
vitest/no-alias-methods: [0]
vitest/no-commented-out-tests: [0]
vitest/no-conditional-expect: [0]
vitest/no-conditional-in-test: [0]
vitest/no-conditional-tests: [0]
vitest/no-disabled-tests: [0]
vitest/no-done-callback: [0]
vitest/no-duplicate-hooks: [0]
vitest/no-focused-tests: [0]
vitest/no-hooks: [0]
vitest/no-identical-title: [2]
vitest/no-interpolation-in-snapshots: [0]
vitest/no-large-snapshots: [0]
vitest/no-mocks-import: [0]
vitest/no-restricted-matchers: [0]
vitest/no-restricted-vi-methods: [0]
vitest/no-standalone-expect: [0]
vitest/no-test-prefixes: [0]
vitest/no-test-return-statement: [0]
vitest/prefer-called-with: [0]
vitest/prefer-comparison-matcher: [0]
vitest/prefer-each: [0]
vitest/prefer-equality-matcher: [0]
vitest/prefer-expect-resolves: [0]
vitest/prefer-hooks-in-order: [0]
vitest/prefer-hooks-on-top: [2]
vitest/prefer-lowercase-title: [0]
vitest/prefer-mock-promise-shorthand: [0]
vitest/prefer-snapshot-hint: [0]
vitest/prefer-spy-on: [0]
vitest/prefer-strict-equal: [0]
vitest/prefer-to-be: [0]
vitest/prefer-to-be-falsy: [0]
vitest/prefer-to-be-object: [0]
vitest/prefer-to-be-truthy: [0]
vitest/prefer-to-contain: [0]
vitest/prefer-to-have-length: [0]
vitest/prefer-todo: [0]
vitest/require-hook: [0]
vitest/require-to-throw-message: [0]
vitest/require-top-level-describe: [0]
vitest/valid-describe-callback: [2]
vitest/valid-expect: [2]
vitest/valid-title: [2]
- files: ["web_src/js/modules/fetch.ts", "web_src/js/standalone/**/*"]
rules:
no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
- files: ["**/*.vue"]
plugins:
- eslint-plugin-vue
- eslint-plugin-vue-scoped-css
extends:
- plugin:vue/vue3-recommended
- plugin:vue-scoped-css/vue3-recommended
rules:
vue/attributes-order: [0]
vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}]
vue/max-attributes-per-line: [0]
vue/singleline-html-element-content-newline: [0]
- files: ["tests/e2e/**"]
plugins:
- eslint-plugin-playwright
extends: plugin:playwright/recommended
rules:
"@eslint-community/eslint-comments/disable-enable-pair": [2]
"@eslint-community/eslint-comments/no-aggregating-enable": [2]
"@eslint-community/eslint-comments/no-duplicate-disable": [2]
"@eslint-community/eslint-comments/no-restricted-disable": [0]
"@eslint-community/eslint-comments/no-unlimited-disable": [2]
"@eslint-community/eslint-comments/no-unused-disable": [2]
"@eslint-community/eslint-comments/no-unused-enable": [2]
"@eslint-community/eslint-comments/no-use": [0]
"@eslint-community/eslint-comments/require-description": [0]
"@stylistic/js/array-bracket-newline": [0]
"@stylistic/js/array-bracket-spacing": [2, never]
"@stylistic/js/array-element-newline": [0]
"@stylistic/js/arrow-parens": [2, always]
"@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, always-multiline]
"@stylistic/js/comma-spacing": [2, {before: false, after: true}]
"@stylistic/js/comma-style": [2, last]
"@stylistic/js/computed-property-spacing": [2, never]
"@stylistic/js/dot-location": [2, property]
"@stylistic/js/eol-last": [2]
"@stylistic/js/function-call-argument-newline": [0]
"@stylistic/js/function-call-spacing": [2, never]
"@stylistic/js/function-paren-newline": [0]
"@stylistic/js/generator-star-spacing": [0]
"@stylistic/js/implicit-arrow-linebreak": [0]
"@stylistic/js/indent": [2, 2, {ignoreComments: true, SwitchCase: 1}]
"@stylistic/js/key-spacing": [2]
"@stylistic/js/keyword-spacing": [2]
"@stylistic/js/line-comment-position": [0]
"@stylistic/js/linebreak-style": [2, unix]
"@stylistic/js/lines-around-comment": [0]
"@stylistic/js/lines-between-class-members": [0]
"@stylistic/js/max-len": [0]
"@stylistic/js/max-statements-per-line": [0]
"@stylistic/js/multiline-comment-style": [0]
"@stylistic/js/multiline-ternary": [0]
"@stylistic/js/new-parens": [2]
"@stylistic/js/newline-per-chained-call": [0]
"@stylistic/js/no-confusing-arrow": [0]
"@stylistic/js/no-extra-parens": [0]
"@stylistic/js/no-extra-semi": [2]
"@stylistic/js/no-floating-decimal": [0]
"@stylistic/js/no-mixed-operators": [0]
"@stylistic/js/no-mixed-spaces-and-tabs": [2]
"@stylistic/js/no-multi-spaces": [2, {ignoreEOLComments: true, exceptions: {Property: true}}]
"@stylistic/js/no-multiple-empty-lines": [2, {max: 1, maxEOF: 0, maxBOF: 0}]
"@stylistic/js/no-tabs": [2]
"@stylistic/js/no-trailing-spaces": [2]
"@stylistic/js/no-whitespace-before-property": [2]
"@stylistic/js/nonblock-statement-body-position": [2]
"@stylistic/js/object-curly-newline": [0]
"@stylistic/js/object-curly-spacing": [2, never]
"@stylistic/js/object-property-newline": [0]
"@stylistic/js/one-var-declaration-per-line": [0]
"@stylistic/js/operator-linebreak": [2, after]
"@stylistic/js/padded-blocks": [2, never]
"@stylistic/js/padding-line-between-statements": [0]
"@stylistic/js/quote-props": [0]
"@stylistic/js/quotes": [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
"@stylistic/js/rest-spread-spacing": [2, never]
"@stylistic/js/semi": [2, always, {omitLastInOneLineBlock: true}]
"@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": [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]
"@stylistic/js/spaced-comment": [2, always]
"@stylistic/js/switch-colon-spacing": [2]
"@stylistic/js/template-curly-spacing": [2, never]
"@stylistic/js/template-tag-spacing": [2, never]
"@stylistic/js/wrap-iife": [2, inside]
"@stylistic/js/wrap-regex": [0]
"@stylistic/js/yield-star-spacing": [2, after]
"@typescript-eslint/adjacent-overload-signatures": [0]
"@typescript-eslint/array-type": [0]
"@typescript-eslint/await-thenable": [2]
"@typescript-eslint/ban-ts-comment": [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}]
"@typescript-eslint/ban-tslint-comment": [0]
"@typescript-eslint/class-literal-property-style": [0]
"@typescript-eslint/class-methods-use-this": [0]
"@typescript-eslint/consistent-generic-constructors": [0]
"@typescript-eslint/consistent-indexed-object-style": [0]
"@typescript-eslint/consistent-return": [0]
"@typescript-eslint/consistent-type-assertions": [2, {assertionStyle: as, objectLiteralTypeAssertions: allow}]
"@typescript-eslint/consistent-type-definitions": [2, type]
"@typescript-eslint/consistent-type-exports": [2, {fixMixedExportsWithInlineTypeSpecifier: false}]
"@typescript-eslint/consistent-type-imports": [2, {prefer: type-imports, fixStyle: separate-type-imports, disallowTypeAnnotations: true}]
"@typescript-eslint/default-param-last": [0]
"@typescript-eslint/dot-notation": [0]
"@typescript-eslint/explicit-function-return-type": [0]
"@typescript-eslint/explicit-member-accessibility": [0]
"@typescript-eslint/explicit-module-boundary-types": [0]
"@typescript-eslint/init-declarations": [0]
"@typescript-eslint/max-params": [0]
"@typescript-eslint/member-ordering": [0]
"@typescript-eslint/method-signature-style": [0]
"@typescript-eslint/naming-convention": [0]
"@typescript-eslint/no-array-constructor": [2]
"@typescript-eslint/no-array-delete": [2]
"@typescript-eslint/no-base-to-string": [0]
"@typescript-eslint/no-confusing-non-null-assertion": [2]
"@typescript-eslint/no-confusing-void-expression": [0]
"@typescript-eslint/no-deprecated": [2]
"@typescript-eslint/no-dupe-class-members": [0]
"@typescript-eslint/no-duplicate-enum-values": [2]
"@typescript-eslint/no-duplicate-type-constituents": [2, {ignoreUnions: true}]
"@typescript-eslint/no-dynamic-delete": [0]
"@typescript-eslint/no-empty-function": [0]
"@typescript-eslint/no-empty-interface": [0]
"@typescript-eslint/no-empty-object-type": [2]
"@typescript-eslint/no-explicit-any": [0]
"@typescript-eslint/no-extra-non-null-assertion": [2]
"@typescript-eslint/no-extraneous-class": [0]
"@typescript-eslint/no-floating-promises": [0]
"@typescript-eslint/no-for-in-array": [2]
"@typescript-eslint/no-implied-eval": [2]
"@typescript-eslint/no-import-type-side-effects": [0] # dupe with consistent-type-imports
"@typescript-eslint/no-inferrable-types": [0]
"@typescript-eslint/no-invalid-this": [0]
"@typescript-eslint/no-invalid-void-type": [0]
"@typescript-eslint/no-loop-func": [0]
"@typescript-eslint/no-loss-of-precision": [0]
"@typescript-eslint/no-magic-numbers": [0]
"@typescript-eslint/no-meaningless-void-operator": [0]
"@typescript-eslint/no-misused-new": [2]
"@typescript-eslint/no-misused-promises": [2, {checksVoidReturn: {attributes: false, arguments: false}}]
"@typescript-eslint/no-mixed-enums": [0]
"@typescript-eslint/no-namespace": [2]
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": [0]
"@typescript-eslint/no-non-null-asserted-optional-chain": [2]
"@typescript-eslint/no-non-null-assertion": [0]
"@typescript-eslint/no-redeclare": [0]
"@typescript-eslint/no-redundant-type-constituents": [2]
"@typescript-eslint/no-require-imports": [2]
"@typescript-eslint/no-restricted-imports": [0]
"@typescript-eslint/no-restricted-types": [0]
"@typescript-eslint/no-shadow": [0]
"@typescript-eslint/no-this-alias": [0] # handled by unicorn/no-this-assignment
"@typescript-eslint/no-unnecessary-boolean-literal-compare": [0]
"@typescript-eslint/no-unnecessary-condition": [0]
"@typescript-eslint/no-unnecessary-qualifier": [0]
"@typescript-eslint/no-unnecessary-template-expression": [0]
"@typescript-eslint/no-unnecessary-type-arguments": [0]
"@typescript-eslint/no-unnecessary-type-assertion": [2]
"@typescript-eslint/no-unnecessary-type-constraint": [2]
"@typescript-eslint/no-unsafe-argument": [0]
"@typescript-eslint/no-unsafe-assignment": [0]
"@typescript-eslint/no-unsafe-call": [0]
"@typescript-eslint/no-unsafe-declaration-merging": [2]
"@typescript-eslint/no-unsafe-enum-comparison": [2]
"@typescript-eslint/no-unsafe-function-type": [2]
"@typescript-eslint/no-unsafe-member-access": [0]
"@typescript-eslint/no-unsafe-return": [0]
"@typescript-eslint/no-unsafe-unary-minus": [2]
"@typescript-eslint/no-unused-expressions": [0]
"@typescript-eslint/no-unused-vars": [2, {vars: all, args: all, caughtErrors: all, ignoreRestSiblings: false, argsIgnorePattern: ^_, varsIgnorePattern: ^_, caughtErrorsIgnorePattern: ^_, destructuredArrayIgnorePattern: ^_}]
"@typescript-eslint/no-use-before-define": [0]
"@typescript-eslint/no-useless-constructor": [0]
"@typescript-eslint/no-useless-empty-export": [0]
"@typescript-eslint/no-wrapper-object-types": [2]
"@typescript-eslint/non-nullable-type-assertion-style": [0]
"@typescript-eslint/only-throw-error": [2]
"@typescript-eslint/parameter-properties": [0]
"@typescript-eslint/prefer-as-const": [2]
"@typescript-eslint/prefer-destructuring": [0]
"@typescript-eslint/prefer-enum-initializers": [0]
"@typescript-eslint/prefer-find": [2]
"@typescript-eslint/prefer-for-of": [2]
"@typescript-eslint/prefer-function-type": [2]
"@typescript-eslint/prefer-includes": [2]
"@typescript-eslint/prefer-literal-enum-member": [0]
"@typescript-eslint/prefer-namespace-keyword": [0]
"@typescript-eslint/prefer-nullish-coalescing": [0]
"@typescript-eslint/prefer-optional-chain": [2, {requireNullish: true}]
"@typescript-eslint/prefer-promise-reject-errors": [0]
"@typescript-eslint/prefer-readonly": [0]
"@typescript-eslint/prefer-readonly-parameter-types": [0]
"@typescript-eslint/prefer-reduce-type-parameter": [0]
"@typescript-eslint/prefer-regexp-exec": [0]
"@typescript-eslint/prefer-return-this-type": [0]
"@typescript-eslint/prefer-string-starts-ends-with": [2, {allowSingleElementEquality: always}]
"@typescript-eslint/promise-function-async": [0]
"@typescript-eslint/require-array-sort-compare": [0]
"@typescript-eslint/require-await": [0]
"@typescript-eslint/restrict-plus-operands": [2]
"@typescript-eslint/restrict-template-expressions": [0]
"@typescript-eslint/return-await": [0]
"@typescript-eslint/strict-boolean-expressions": [0]
"@typescript-eslint/switch-exhaustiveness-check": [0]
"@typescript-eslint/triple-slash-reference": [2]
"@typescript-eslint/typedef": [0]
"@typescript-eslint/unbound-method": [0] # too many false-positives
"@typescript-eslint/unified-signatures": [2]
accessor-pairs: [2]
array-callback-return: [2, {checkForEach: true}]
array-func/avoid-reverse: [2]
array-func/from-map: [2]
array-func/no-unnecessary-this-arg: [2]
array-func/prefer-array-from: [2]
array-func/prefer-flat-map: [0] # handled by unicorn/prefer-array-flat-map
array-func/prefer-flat: [0] # handled by unicorn/prefer-array-flat
arrow-body-style: [0]
block-scoped-var: [2]
camelcase: [0]
capitalized-comments: [0]
class-methods-use-this: [0]
complexity: [0]
consistent-return: [0]
consistent-this: [0]
constructor-super: [2]
curly: [0]
default-case-last: [2]
default-case: [0]
default-param-last: [0]
dot-notation: [0]
eqeqeq: [2]
for-direction: [2]
func-name-matching: [2]
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]
id-length: [0]
id-match: [0]
import-x/consistent-type-specifier-style: [0]
import-x/default: [0]
import-x/dynamic-import-chunkname: [0]
import-x/export: [2]
import-x/exports-last: [0]
import-x/extensions: [2, always, {ignorePackages: true}]
import-x/first: [2]
import-x/group-exports: [0]
import-x/max-dependencies: [0]
import-x/named: [2]
import-x/namespace: [0]
import-x/newline-after-import: [0]
import-x/no-absolute-path: [0]
import-x/no-amd: [2]
import-x/no-anonymous-default-export: [0]
import-x/no-commonjs: [2]
import-x/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}]
import-x/no-default-export: [0]
import-x/no-deprecated: [0]
import-x/no-dynamic-require: [0]
import-x/no-empty-named-blocks: [2]
import-x/no-extraneous-dependencies: [2]
import-x/no-import-module-exports: [0]
import-x/no-internal-modules: [0]
import-x/no-mutable-exports: [0]
import-x/no-named-as-default-member: [0]
import-x/no-named-as-default: [0]
import-x/no-named-default: [0]
import-x/no-named-export: [0]
import-x/no-namespace: [0]
import-x/no-nodejs-modules: [0]
import-x/no-relative-packages: [0]
import-x/no-relative-parent-imports: [0]
import-x/no-restricted-paths: [0]
import-x/no-self-import: [2]
import-x/no-unassigned-import: [0]
import-x/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}]
import-x/no-unused-modules: [2, {unusedExports: true}]
import-x/no-useless-path-segments: [2, {commonjs: true}]
import-x/no-webpack-loader-syntax: [2]
import-x/order: [0]
import-x/prefer-default-export: [0]
import-x/unambiguous: [0]
init-declarations: [0]
line-comment-position: [0]
logical-assignment-operators: [0]
max-classes-per-file: [0]
max-depth: [0]
max-lines-per-function: [0]
max-lines: [0]
max-nested-callbacks: [0]
max-params: [0]
max-statements: [0]
multiline-comment-style: [2, separate-lines]
new-cap: [0]
no-alert: [0]
no-array-constructor: [0] # handled by @typescript-eslint/no-array-constructor
no-async-promise-executor: [0]
no-await-in-loop: [0]
no-bitwise: [0]
no-buffer-constructor: [0]
no-caller: [2]
no-case-declarations: [2]
no-class-assign: [2]
no-compare-neg-zero: [2]
no-cond-assign: [2, except-parens]
no-console: [1, {allow: [debug, info, warn, error]}]
no-const-assign: [2]
no-constant-binary-expression: [2]
no-constant-condition: [0]
no-constructor-return: [2]
no-continue: [0]
no-control-regex: [0]
no-debugger: [1]
no-delete-var: [2]
no-div-regex: [0]
no-dupe-args: [2]
no-dupe-class-members: [2]
no-dupe-else-if: [2]
no-dupe-keys: [2]
no-duplicate-case: [2]
no-duplicate-imports: [0]
no-else-return: [2]
no-empty-character-class: [2]
no-empty-function: [0]
no-empty-pattern: [2]
no-empty-static-block: [2]
no-empty: [2, {allowEmptyCatch: true}]
no-eq-null: [2]
no-eval: [2]
no-ex-assign: [2]
no-extend-native: [2]
no-extra-bind: [2]
no-extra-boolean-cast: [2]
no-extra-label: [0]
no-fallthrough: [2]
no-func-assign: [2]
no-global-assign: [2]
no-implicit-coercion: [2]
no-implicit-globals: [0]
no-implied-eval: [0] # handled by @typescript-eslint/no-implied-eval
no-import-assign: [2]
no-inline-comments: [0]
no-inner-declarations: [2]
no-invalid-regexp: [2]
no-invalid-this: [0]
no-irregular-whitespace: [2]
no-iterator: [2]
no-jquery/no-ajax-events: [2]
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: [2]
no-jquery/no-attr: [2]
no-jquery/no-bind: [2]
no-jquery/no-box-model: [2]
no-jquery/no-browser: [2]
no-jquery/no-camel-case: [2]
no-jquery/no-class-state: [2]
no-jquery/no-class: [0]
no-jquery/no-clone: [2]
no-jquery/no-closest: [0]
no-jquery/no-constructor-attributes: [2]
no-jquery/no-contains: [2]
no-jquery/no-context-prop: [2]
no-jquery/no-css: [2]
no-jquery/no-data: [0]
no-jquery/no-deferred: [2]
no-jquery/no-delegate: [2]
no-jquery/no-done-fail: [2]
no-jquery/no-each-collection: [0]
no-jquery/no-each-util: [0]
no-jquery/no-each: [0]
no-jquery/no-error-shorthand: [2]
no-jquery/no-error: [2]
no-jquery/no-escape-selector: [2]
no-jquery/no-event-shorthand: [2]
no-jquery/no-extend: [2]
no-jquery/no-fade: [2]
no-jquery/no-filter: [0]
no-jquery/no-find-collection: [0]
no-jquery/no-find-util: [2]
no-jquery/no-find: [0]
no-jquery/no-fx-interval: [2]
no-jquery/no-fx: [2]
no-jquery/no-global-eval: [2]
no-jquery/no-global-selector: [0]
no-jquery/no-grep: [2]
no-jquery/no-has: [2]
no-jquery/no-hold-ready: [2]
no-jquery/no-html: [0]
no-jquery/no-in-array: [2]
no-jquery/no-is-array: [2]
no-jquery/no-is-empty-object: [2]
no-jquery/no-is-function: [2]
no-jquery/no-is-numeric: [2]
no-jquery/no-is-plain-object: [2]
no-jquery/no-is-window: [2]
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: [2]
no-jquery/no-merge: [2]
no-jquery/no-node-name: [2]
no-jquery/no-noop: [2]
no-jquery/no-now: [2]
no-jquery/no-on-ready: [2]
no-jquery/no-other-methods: [0]
no-jquery/no-other-utils: [2]
no-jquery/no-param: [2]
no-jquery/no-parent: [0]
no-jquery/no-parents: [2]
no-jquery/no-parse-html-literal: [2]
no-jquery/no-parse-html: [2]
no-jquery/no-parse-json: [2]
no-jquery/no-parse-xml: [2]
no-jquery/no-prop: [2]
no-jquery/no-proxy: [2]
no-jquery/no-ready-shorthand: [2]
no-jquery/no-ready: [2]
no-jquery/no-selector-prop: [2]
no-jquery/no-serialize: [2]
no-jquery/no-size: [2]
no-jquery/no-sizzle: [2]
no-jquery/no-slide: [2]
no-jquery/no-sub: [2]
no-jquery/no-support: [2]
no-jquery/no-text: [2]
no-jquery/no-trigger: [0]
no-jquery/no-trim: [2]
no-jquery/no-type: [2]
no-jquery/no-unique: [2]
no-jquery/no-unload-shorthand: [2]
no-jquery/no-val: [0]
no-jquery/no-visibility: [2]
no-jquery/no-when: [2]
no-jquery/no-wrap: [2]
no-jquery/variable-pattern: [2]
no-label-var: [2]
no-labels: [0] # handled by no-restricted-syntax
no-lone-blocks: [2]
no-lonely-if: [0]
no-loop-func: [0]
no-loss-of-precision: [2]
no-magic-numbers: [0]
no-misleading-character-class: [2]
no-multi-assign: [0]
no-multi-str: [2]
no-negated-condition: [0]
no-nested-ternary: [0]
no-new-func: [2]
no-new-native-nonconstructor: [2]
no-new-object: [2]
no-new-symbol: [2]
no-new-wrappers: [2]
no-new: [0]
no-nonoctal-decimal-escape: [2]
no-obj-calls: [2]
no-octal-escape: [2]
no-octal: [2]
no-param-reassign: [0]
no-plusplus: [0]
no-promise-executor-return: [0]
no-proto: [2]
no-prototype-builtins: [2]
no-redeclare: [0] # must be disabled for typescript overloads
no-regex-spaces: [2]
no-restricted-exports: [0]
no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, 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, self, status, statusbar, stop, toolbar, top, __dirname, __filename]
no-restricted-imports: [0]
no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.ts instead"}]
no-return-assign: [0]
no-script-url: [2]
no-self-assign: [2, {props: true}]
no-self-compare: [2]
no-sequences: [2]
no-setter-return: [2]
no-shadow-restricted-names: [2]
no-shadow: [0]
no-sparse-arrays: [2]
no-template-curly-in-string: [2]
no-ternary: [0]
no-this-before-super: [2]
no-throw-literal: [2]
no-undef-init: [2]
no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes
no-undefined: [0]
no-underscore-dangle: [0]
no-unexpected-multiline: [2]
no-unmodified-loop-condition: [2]
no-unneeded-ternary: [2]
no-unreachable-loop: [2]
no-unreachable: [2]
no-unsafe-finally: [2]
no-unsafe-negation: [2]
no-unused-expressions: [2]
no-unused-labels: [2]
no-unused-private-class-members: [2]
no-unused-vars: [0] # handled by @typescript-eslint/no-unused-vars
no-use-before-define: [2, {functions: false, classes: true, variables: true, allowNamedExports: true}]
no-use-extend-native/no-use-extend-native: [2]
no-useless-backreference: [2]
no-useless-call: [2]
no-useless-catch: [2]
no-useless-computed-key: [2]
no-useless-concat: [2]
no-useless-constructor: [2]
no-useless-escape: [2]
no-useless-rename: [2]
no-useless-return: [2]
no-var: [2]
no-void: [2]
no-warning-comments: [0]
no-with: [0] # handled by no-restricted-syntax
object-shorthand: [2, always]
one-var-declaration-per-line: [0]
one-var: [0]
operator-assignment: [2, always]
operator-linebreak: [2, after]
prefer-arrow-callback: [2, {allowNamedFunctions: true, allowUnboundThis: true}]
prefer-const: [2, {destructuring: all, ignoreReadBeforeAssign: true}]
prefer-destructuring: [0]
prefer-exponentiation-operator: [2]
prefer-named-capture-group: [0]
prefer-numeric-literals: [2]
prefer-object-has-own: [2]
prefer-object-spread: [2]
prefer-promise-reject-errors: [2, {allowEmptyReject: false}]
prefer-regex-literals: [2]
prefer-rest-params: [2]
prefer-spread: [2]
prefer-template: [2]
radix: [2, as-needed]
regexp/confusing-quantifier: [2]
regexp/control-character-escape: [2]
regexp/hexadecimal-escape: [0]
regexp/letter-case: [0]
regexp/match-any: [2]
regexp/negation: [2]
regexp/no-contradiction-with-assertion: [0]
regexp/no-control-character: [0]
regexp/no-dupe-characters-character-class: [2]
regexp/no-dupe-disjunctions: [2]
regexp/no-empty-alternative: [2]
regexp/no-empty-capturing-group: [2]
regexp/no-empty-character-class: [0]
regexp/no-empty-group: [2]
regexp/no-empty-lookarounds-assertion: [2]
regexp/no-empty-string-literal: [2]
regexp/no-escape-backspace: [2]
regexp/no-extra-lookaround-assertions: [0]
regexp/no-invalid-regexp: [2]
regexp/no-invisible-character: [2]
regexp/no-lazy-ends: [2]
regexp/no-legacy-features: [2]
regexp/no-misleading-capturing-group: [0]
regexp/no-misleading-unicode-character: [0]
regexp/no-missing-g-flag: [2]
regexp/no-non-standard-flag: [2]
regexp/no-obscure-range: [2]
regexp/no-octal: [2]
regexp/no-optional-assertion: [2]
regexp/no-potentially-useless-backreference: [2]
regexp/no-standalone-backslash: [2]
regexp/no-super-linear-backtracking: [0]
regexp/no-super-linear-move: [0]
regexp/no-trivially-nested-assertion: [2]
regexp/no-trivially-nested-quantifier: [2]
regexp/no-unused-capturing-group: [0]
regexp/no-useless-assertions: [2]
regexp/no-useless-backreference: [2]
regexp/no-useless-character-class: [2]
regexp/no-useless-dollar-replacements: [2]
regexp/no-useless-escape: [2]
regexp/no-useless-flag: [2]
regexp/no-useless-lazy: [2]
regexp/no-useless-non-capturing-group: [2]
regexp/no-useless-quantifier: [2]
regexp/no-useless-range: [2]
regexp/no-useless-set-operand: [2]
regexp/no-useless-string-literal: [2]
regexp/no-useless-two-nums-quantifier: [2]
regexp/no-zero-quantifier: [2]
regexp/optimal-lookaround-quantifier: [2]
regexp/optimal-quantifier-concatenation: [0]
regexp/prefer-character-class: [0]
regexp/prefer-d: [0]
regexp/prefer-escape-replacement-dollar-char: [0]
regexp/prefer-lookaround: [0]
regexp/prefer-named-backreference: [0]
regexp/prefer-named-capture-group: [0]
regexp/prefer-named-replacement: [0]
regexp/prefer-plus-quantifier: [2]
regexp/prefer-predefined-assertion: [2]
regexp/prefer-quantifier: [0]
regexp/prefer-question-quantifier: [2]
regexp/prefer-range: [2]
regexp/prefer-regexp-exec: [2]
regexp/prefer-regexp-test: [2]
regexp/prefer-result-array-groups: [0]
regexp/prefer-set-operation: [2]
regexp/prefer-star-quantifier: [2]
regexp/prefer-unicode-codepoint-escapes: [2]
regexp/prefer-w: [0]
regexp/require-unicode-regexp: [0]
regexp/simplify-set-operations: [2]
regexp/sort-alternatives: [0]
regexp/sort-character-class-elements: [0]
regexp/sort-flags: [0]
regexp/strict: [2]
regexp/unicode-escape: [0]
regexp/use-ignore-case: [0]
require-atomic-updates: [0]
require-await: [0] # handled by @typescript-eslint/require-await
require-unicode-regexp: [0]
require-yield: [2]
sonarjs/cognitive-complexity: [0]
sonarjs/elseif-without-else: [0]
sonarjs/max-switch-cases: [0]
sonarjs/no-all-duplicated-branches: [2]
sonarjs/no-collapsible-if: [0]
sonarjs/no-collection-size-mischeck: [2]
sonarjs/no-duplicate-string: [0]
sonarjs/no-duplicated-branches: [0]
sonarjs/no-element-overwrite: [2]
sonarjs/no-empty-collection: [2]
sonarjs/no-extra-arguments: [2]
sonarjs/no-gratuitous-expressions: [2]
sonarjs/no-identical-conditions: [2]
sonarjs/no-identical-expressions: [2]
sonarjs/no-identical-functions: [2, 5]
sonarjs/no-ignored-return: [2]
sonarjs/no-inverted-boolean-check: [2]
sonarjs/no-nested-switch: [0]
sonarjs/no-nested-template-literals: [0]
sonarjs/no-one-iteration-loop: [2]
sonarjs/no-redundant-boolean: [2]
sonarjs/no-redundant-jump: [2]
sonarjs/no-same-line-conditional: [2]
sonarjs/no-small-switch: [0]
sonarjs/no-unused-collection: [2]
sonarjs/no-use-of-empty-return-value: [2]
sonarjs/no-useless-catch: [2]
sonarjs/non-existent-operator: [2]
sonarjs/prefer-immediate-return: [0]
sonarjs/prefer-object-literal: [0]
sonarjs/prefer-single-boolean-return: [0]
sonarjs/prefer-while: [2]
sort-imports: [0]
sort-keys: [0]
sort-vars: [0]
strict: [0]
symbol-description: [2]
unicode-bom: [2, never]
unicorn/better-regex: [0]
unicorn/catch-error-name: [0]
unicorn/consistent-destructuring: [2]
unicorn/consistent-empty-array-spread: [2]
unicorn/consistent-existence-index-check: [0]
unicorn/consistent-function-scoping: [0]
unicorn/custom-error-definition: [0]
unicorn/empty-brace-spaces: [2]
unicorn/error-message: [0]
unicorn/escape-case: [0]
unicorn/expiring-todo-comments: [0]
unicorn/explicit-length-check: [0]
unicorn/filename-case: [0]
unicorn/import-index: [0]
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]
unicorn/no-for-loop: [0]
unicorn/no-hex-escape: [0]
unicorn/no-instanceof-array: [0]
unicorn/no-invalid-fetch-options: [2]
unicorn/no-invalid-remove-event-listener: [2]
unicorn/no-keyword-prefix: [0]
unicorn/no-length-as-slice-end: [2]
unicorn/no-lonely-if: [2]
unicorn/no-magic-array-flat-depth: [0]
unicorn/no-negated-condition: [0]
unicorn/no-negation-in-equality-check: [2]
unicorn/no-nested-ternary: [0]
unicorn/no-new-array: [0]
unicorn/no-new-buffer: [0]
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]
unicorn/no-typeof-undefined: [2]
unicorn/no-unnecessary-await: [2]
unicorn/no-unnecessary-polyfills: [2]
unicorn/no-unreadable-array-destructuring: [0]
unicorn/no-unreadable-iife: [2]
unicorn/no-unused-properties: [2]
unicorn/no-useless-fallback-in-spread: [2]
unicorn/no-useless-length-check: [2]
unicorn/no-useless-promise-resolve-reject: [2]
unicorn/no-useless-spread: [2]
unicorn/no-useless-switch-case: [2]
unicorn/no-useless-undefined: [0]
unicorn/no-zero-fractions: [2]
unicorn/number-literal-case: [0]
unicorn/numeric-separators-style: [0]
unicorn/prefer-add-event-listener: [2]
unicorn/prefer-array-find: [2]
unicorn/prefer-array-flat-map: [2]
unicorn/prefer-array-flat: [2]
unicorn/prefer-array-index-of: [2]
unicorn/prefer-array-some: [2]
unicorn/prefer-at: [0]
unicorn/prefer-blob-reading-methods: [2]
unicorn/prefer-code-point: [0]
unicorn/prefer-date-now: [2]
unicorn/prefer-default-parameters: [0]
unicorn/prefer-dom-node-append: [2]
unicorn/prefer-dom-node-dataset: [0]
unicorn/prefer-dom-node-remove: [2]
unicorn/prefer-dom-node-text-content: [2]
unicorn/prefer-event-target: [2]
unicorn/prefer-export-from: [0]
unicorn/prefer-global-this: [0]
unicorn/prefer-includes: [2]
unicorn/prefer-json-parse-buffer: [0]
unicorn/prefer-keyboard-event-key: [2]
unicorn/prefer-logical-operator-over-ternary: [2]
unicorn/prefer-math-min-max: [2]
unicorn/prefer-math-trunc: [2]
unicorn/prefer-modern-dom-apis: [0]
unicorn/prefer-modern-math-apis: [2]
unicorn/prefer-module: [2]
unicorn/prefer-native-coercion-functions: [2]
unicorn/prefer-negative-index: [2]
unicorn/prefer-node-protocol: [2]
unicorn/prefer-number-properties: [0]
unicorn/prefer-object-from-entries: [2]
unicorn/prefer-object-has-own: [0]
unicorn/prefer-optional-catch-binding: [2]
unicorn/prefer-prototype-methods: [0]
unicorn/prefer-query-selector: [2]
unicorn/prefer-reflect-apply: [0]
unicorn/prefer-regexp-test: [2]
unicorn/prefer-set-has: [0]
unicorn/prefer-set-size: [2]
unicorn/prefer-spread: [0]
unicorn/prefer-string-raw: [0]
unicorn/prefer-string-replace-all: [0]
unicorn/prefer-string-slice: [0]
unicorn/prefer-string-starts-ends-with: [2]
unicorn/prefer-string-trim-start-end: [2]
unicorn/prefer-structured-clone: [2]
unicorn/prefer-switch: [0]
unicorn/prefer-ternary: [0]
unicorn/prefer-text-content: [2]
unicorn/prefer-top-level-await: [0]
unicorn/prefer-type-error: [0]
unicorn/prevent-abbreviations: [0]
unicorn/relative-url-style: [2]
unicorn/require-array-join-separator: [2]
unicorn/require-number-to-fixed-digits-argument: [2]
unicorn/require-post-message-target-origin: [0]
unicorn/string-content: [0]
unicorn/switch-case-braces: [0]
unicorn/template-indent: [2]
unicorn/text-encoding-identifier-case: [0]
unicorn/throw-new-error: [2]
use-isnan: [2]
valid-typeof: [2, {requireStringLiterals: true}]
vars-on-top: [0]
wc/attach-shadow-constructor: [2]
wc/define-tag-after-class-definition: [0]
wc/expose-class-on-global: [0]
wc/file-name-matches-element: [2]
wc/guard-define-call: [0]
wc/guard-super-call: [2]
wc/max-elements-per-file: [0]
wc/no-child-traversal-in-attributechangedcallback: [2]
wc/no-child-traversal-in-connectedcallback: [2]
wc/no-closed-shadow-root: [2]
wc/no-constructor-attributes: [2]
wc/no-constructor-params: [2]
wc/no-constructor: [2]
wc/no-customized-built-in-elements: [2]
wc/no-exports-with-element: [0]
wc/no-invalid-element-name: [2]
wc/no-invalid-extends: [2]
wc/no-method-prefixed-with-on: [2]
wc/no-self-class: [2]
wc/no-typos: [2]
wc/require-listener-teardown: [2]
wc/tag-name-matches-class: [2]
yoda: [2, never]

View File

@ -1,5 +1,5 @@
# Build stage # Build stage
FROM docker.io/library/golang:1.23-alpine3.20 AS build-env FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
ARG GOPROXY ARG GOPROXY
ENV GOPROXY=${GOPROXY:-direct} ENV GOPROXY=${GOPROXY:-direct}
@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/go/src/code.gitea.io/gitea/environment-to-ini /go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
FROM docker.io/library/alpine:3.20 FROM docker.io/library/alpine:3.21
LABEL maintainer="maintainers@gitea.io" LABEL maintainer="maintainers@gitea.io"
EXPOSE 22 3000 EXPOSE 22 3000
@ -78,7 +78,7 @@ ENV GITEA_CUSTOM=/data/gitea
VOLUME ["/data"] VOLUME ["/data"]
ENTRYPOINT ["/usr/bin/entrypoint"] ENTRYPOINT ["/usr/bin/entrypoint"]
CMD ["/bin/s6-svscan", "/etc/s6"] CMD ["/usr/bin/s6-svscan", "/etc/s6"]
COPY --from=build-env /tmp/local / COPY --from=build-env /tmp/local /
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea

View File

@ -1,5 +1,5 @@
# Build stage # Build stage
FROM docker.io/library/golang:1.23-alpine3.20 AS build-env FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
ARG GOPROXY ARG GOPROXY
ENV GOPROXY=${GOPROXY:-direct} ENV GOPROXY=${GOPROXY:-direct}
@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
/go/src/code.gitea.io/gitea/environment-to-ini /go/src/code.gitea.io/gitea/environment-to-ini
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
FROM docker.io/library/alpine:3.20 FROM docker.io/library/alpine:3.21
LABEL maintainer="maintainers@gitea.io" LABEL maintainer="maintainers@gitea.io"
EXPOSE 2222 3000 EXPOSE 2222 3000

View File

@ -46,7 +46,6 @@ Wim <wim@42.be> (@42wim)
Jason Song <i@wolfogre.com> (@wolfogre) Jason Song <i@wolfogre.com> (@wolfogre)
Yarden Shoham <git@yardenshoham.com> (@yardenshoham) Yarden Shoham <git@yardenshoham.com> (@yardenshoham)
Yu Tian <zettat123@gmail.com> (@Zettat123) Yu Tian <zettat123@gmail.com> (@Zettat123)
Eddie Yang <576951401@qq.com> (@yp05327)
Dong Ge <gedong_1994@163.com> (@sillyguodong) Dong Ge <gedong_1994@163.com> (@sillyguodong)
Xinyi Gong <hestergong@gmail.com> (@HesterG) Xinyi Gong <hestergong@gmail.com> (@HesterG)
wxiaoguang <wxiaoguang@gmail.com> (@wxiaoguang) wxiaoguang <wxiaoguang@gmail.com> (@wxiaoguang)

View File

@ -26,17 +26,17 @@ COMMA := ,
XGO_VERSION := go-1.23.x XGO_VERSION := go-1.23.x
AIR_PACKAGE ?= github.com/air-verse/air@v1 AIR_PACKAGE ?= github.com/air-verse/air@v1
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.0.3
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3 GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.0
DOCKER_IMAGE ?= gitea/gitea DOCKER_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest DOCKER_TAG ?= latest

2
go.mod
View File

@ -123,7 +123,7 @@ require (
github.com/yuin/goldmark-meta v1.1.0 github.com/yuin/goldmark-meta v1.1.0
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.31.0
golang.org/x/image v0.21.0 golang.org/x/image v0.21.0
golang.org/x/net v0.30.0 golang.org/x/net v0.33.0
golang.org/x/oauth2 v0.23.0 golang.org/x/oauth2 v0.23.0
golang.org/x/sync v0.10.0 golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0 golang.org/x/sys v0.28.0

4
go.sum
View File

@ -932,8 +932,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -153,28 +153,34 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
} }
func AggregateJobStatus(jobs []*ActionRunJob) Status { func AggregateJobStatus(jobs []*ActionRunJob) Status {
allDone := true allSuccessOrSkipped := len(jobs) != 0
allWaiting := true allSkipped := len(jobs) != 0
hasFailure := false var hasFailure, hasCancelled, hasWaiting, hasRunning, hasBlocked bool
for _, job := range jobs { for _, job := range jobs {
if !job.Status.IsDone() { allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped)
allDone = false allSkipped = allSkipped && job.Status == StatusSkipped
} hasFailure = hasFailure || job.Status == StatusFailure
if job.Status != StatusWaiting && !job.Status.IsDone() { hasCancelled = hasCancelled || job.Status == StatusCancelled
allWaiting = false hasWaiting = hasWaiting || job.Status == StatusWaiting
} hasRunning = hasRunning || job.Status == StatusRunning
if job.Status == StatusFailure || job.Status == StatusCancelled { hasBlocked = hasBlocked || job.Status == StatusBlocked
hasFailure = true
}
}
if allDone {
if hasFailure {
return StatusFailure
} }
switch {
case allSkipped:
return StatusSkipped
case allSuccessOrSkipped:
return StatusSuccess return StatusSuccess
} case hasCancelled:
if allWaiting { return StatusCancelled
return StatusWaiting case hasFailure:
} return StatusFailure
case hasRunning:
return StatusRunning return StatusRunning
case hasWaiting:
return StatusWaiting
case hasBlocked:
return StatusBlocked
default:
return StatusUnknown // it shouldn't happen
}
} }

View File

@ -0,0 +1,85 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAggregateJobStatus(t *testing.T) {
testStatuses := func(expected Status, statuses []Status) {
t.Helper()
var jobs []*ActionRunJob
for _, v := range statuses {
jobs = append(jobs, &ActionRunJob{Status: v})
}
actual := AggregateJobStatus(jobs)
if !assert.Equal(t, expected, actual) {
var statusStrings []string
for _, s := range statuses {
statusStrings = append(statusStrings, s.String())
}
t.Errorf("AggregateJobStatus(%v) = %v, want %v", statusStrings, statusNames[actual], statusNames[expected])
}
}
cases := []struct {
statuses []Status
expected Status
}{
// unknown cases, maybe it shouldn't happen in real world
{[]Status{}, StatusUnknown},
{[]Status{StatusUnknown, StatusSuccess}, StatusUnknown},
{[]Status{StatusUnknown, StatusSkipped}, StatusUnknown},
{[]Status{StatusUnknown, StatusFailure}, StatusFailure},
{[]Status{StatusUnknown, StatusCancelled}, StatusCancelled},
{[]Status{StatusUnknown, StatusWaiting}, StatusWaiting},
{[]Status{StatusUnknown, StatusRunning}, StatusRunning},
{[]Status{StatusUnknown, StatusBlocked}, StatusBlocked},
// success with other status
{[]Status{StatusSuccess}, StatusSuccess},
{[]Status{StatusSuccess, StatusSkipped}, StatusSuccess}, // skipped doesn't affect success
{[]Status{StatusSuccess, StatusFailure}, StatusFailure},
{[]Status{StatusSuccess, StatusCancelled}, StatusCancelled},
{[]Status{StatusSuccess, StatusWaiting}, StatusWaiting},
{[]Status{StatusSuccess, StatusRunning}, StatusRunning},
{[]Status{StatusSuccess, StatusBlocked}, StatusBlocked},
// any cancelled, then cancelled
{[]Status{StatusCancelled}, StatusCancelled},
{[]Status{StatusCancelled, StatusSuccess}, StatusCancelled},
{[]Status{StatusCancelled, StatusSkipped}, StatusCancelled},
{[]Status{StatusCancelled, StatusFailure}, StatusCancelled},
{[]Status{StatusCancelled, StatusWaiting}, StatusCancelled},
{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
// failure with other status, fail fast
// Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast.
{[]Status{StatusFailure}, StatusFailure},
{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
{[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
{[]Status{StatusFailure, StatusRunning}, StatusFailure},
{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
// skipped with other status
// "all skipped" is also considered as "mergeable" by "services/actions.toCommitStatus", the same as GitHub
{[]Status{StatusSkipped}, StatusSkipped},
{[]Status{StatusSkipped, StatusSuccess}, StatusSuccess},
{[]Status{StatusSkipped, StatusFailure}, StatusFailure},
{[]Status{StatusSkipped, StatusCancelled}, StatusCancelled},
{[]Status{StatusSkipped, StatusWaiting}, StatusWaiting},
{[]Status{StatusSkipped, StatusRunning}, StatusRunning},
{[]Status{StatusSkipped, StatusBlocked}, StatusBlocked},
}
for _, c := range cases {
testStatuses(c.expected, c.statuses)
}
}

View File

@ -511,7 +511,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
} }
if opts.RequestedTeam != nil { if opts.RequestedTeam != nil {
env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(ctx, opts.RequestedTeam) env := repo_model.AccessibleTeamReposEnv(ctx, organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetTeamRepositories: %w", err) return nil, fmt.Errorf("GetTeamRepositories: %w", err)

View File

@ -217,6 +217,7 @@ func (err ErrGPGKeyAccessDenied) Unwrap() error {
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
type ErrKeyAccessDenied struct { type ErrKeyAccessDenied struct {
UserID int64 UserID int64
RepoID int64
KeyID int64 KeyID int64
Note string Note string
} }
@ -228,8 +229,8 @@ func IsErrKeyAccessDenied(err error) bool {
} }
func (err ErrKeyAccessDenied) Error() string { func (err ErrKeyAccessDenied) Error() string {
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]", return fmt.Sprintf("user does not have access to the key [user_id: %d, repo_id: %d, key_id: %d, note: %s]",
err.UserID, err.KeyID, err.Note) err.UserID, err.RepoID, err.KeyID, err.Note)
} }
func (err ErrKeyAccessDenied) Unwrap() error { func (err ErrKeyAccessDenied) Unwrap() error {

View File

@ -13,6 +13,8 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
"xorm.io/xorm/schemas" "xorm.io/xorm/schemas"
) )
@ -54,7 +56,8 @@ func TestDumpAuthSource(t *testing.T) {
sb := new(strings.Builder) sb := new(strings.Builder)
db.DumpTables([]*schemas.Table{authSourceSchema}, sb) // TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump
engine := db.GetEngine(db.DefaultContext).(*xorm.Engine)
require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb))
assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`) assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`)
} }

View File

@ -140,7 +140,7 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
} }
func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) { func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) {
return CheckCollations(x) return CheckCollations(xormEngine)
} }
func alterDatabaseCollation(x *xorm.Engine, collation string) error { func alterDatabaseCollation(x *xorm.Engine, collation string) error {

View File

@ -94,7 +94,7 @@ func GetEngine(ctx context.Context) Engine {
if e := getExistingEngine(ctx); e != nil { if e := getExistingEngine(ctx); e != nil {
return e return e
} }
return x.Context(ctx) return xormEngine.Context(ctx)
} }
// getExistingEngine gets an existing db Engine/Statement from this context or returns nil // getExistingEngine gets an existing db Engine/Statement from this context or returns nil
@ -155,7 +155,7 @@ func TxContext(parentCtx context.Context) (*Context, Committer, error) {
return newContext(parentCtx, sess), &halfCommitter{committer: sess}, nil return newContext(parentCtx, sess), &halfCommitter{committer: sess}, nil
} }
sess := x.NewSession() sess := xormEngine.NewSession()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
_ = sess.Close() _ = sess.Close()
return nil, nil, err return nil, nil, err
@ -179,7 +179,7 @@ func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error
} }
func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error { func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error {
sess := x.NewSession() sess := xormEngine.NewSession()
defer sess.Close() defer sess.Close()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
return err return err
@ -322,7 +322,7 @@ func CountByBean(ctx context.Context, bean any) (int64, error) {
// TableName returns the table name according a bean object // TableName returns the table name according a bean object
func TableName(bean any) string { func TableName(bean any) string {
return x.TableName(bean) return xormEngine.TableName(bean)
} }
// InTransaction returns true if the engine is in a transaction otherwise return false // InTransaction returns true if the engine is in a transaction otherwise return false

View File

@ -16,30 +16,30 @@ import (
// ConvertDatabaseTable converts database and tables from utf8 to utf8mb4 if it's mysql and set ROW_FORMAT=dynamic // ConvertDatabaseTable converts database and tables from utf8 to utf8mb4 if it's mysql and set ROW_FORMAT=dynamic
func ConvertDatabaseTable() error { func ConvertDatabaseTable() error {
if x.Dialect().URI().DBType != schemas.MYSQL { if xormEngine.Dialect().URI().DBType != schemas.MYSQL {
return nil return nil
} }
r, err := CheckCollations(x) r, err := CheckCollations(xormEngine)
if err != nil { if err != nil {
return err return err
} }
_, err = x.Exec(fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET utf8mb4 COLLATE %s", setting.Database.Name, r.ExpectedCollation)) _, err = xormEngine.Exec(fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET utf8mb4 COLLATE %s", setting.Database.Name, r.ExpectedCollation))
if err != nil { if err != nil {
return err return err
} }
tables, err := x.DBMetas() tables, err := xormEngine.DBMetas()
if err != nil { if err != nil {
return err return err
} }
for _, table := range tables { for _, table := range tables {
if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` ROW_FORMAT=dynamic", table.Name)); err != nil { if _, err := xormEngine.Exec(fmt.Sprintf("ALTER TABLE `%s` ROW_FORMAT=dynamic", table.Name)); err != nil {
return err return err
} }
if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE %s", table.Name, r.ExpectedCollation)); err != nil { if _, err := xormEngine.Exec(fmt.Sprintf("ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE %s", table.Name, r.ExpectedCollation)); err != nil {
return err return err
} }
} }
@ -49,11 +49,11 @@ func ConvertDatabaseTable() error {
// ConvertVarcharToNVarchar converts database and tables from varchar to nvarchar if it's mssql // ConvertVarcharToNVarchar converts database and tables from varchar to nvarchar if it's mssql
func ConvertVarcharToNVarchar() error { func ConvertVarcharToNVarchar() error {
if x.Dialect().URI().DBType != schemas.MSSQL { if xormEngine.Dialect().URI().DBType != schemas.MSSQL {
return nil return nil
} }
sess := x.NewSession() sess := xormEngine.NewSession()
defer sess.Close() defer sess.Close()
res, err := sess.QuerySliceString(`SELECT 'ALTER TABLE ' + OBJECT_NAME(SC.object_id) + ' MODIFY SC.name NVARCHAR(' + CONVERT(VARCHAR(5),SC.max_length) + ')' res, err := sess.QuerySliceString(`SELECT 'ALTER TABLE ' + OBJECT_NAME(SC.object_id) + ' MODIFY SC.name NVARCHAR(' + CONVERT(VARCHAR(5),SC.max_length) + ')'
FROM SYS.columns SC FROM SYS.columns SC

View File

@ -8,17 +8,10 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"io"
"reflect" "reflect"
"strings" "strings"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm" "xorm.io/xorm"
"xorm.io/xorm/contexts"
"xorm.io/xorm/names"
"xorm.io/xorm/schemas" "xorm.io/xorm/schemas"
_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver _ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
@ -27,9 +20,9 @@ import (
) )
var ( var (
x *xorm.Engine xormEngine *xorm.Engine
tables []any registeredModels []any
initFuncs []func() error registeredInitFuncs []func() error
) )
// Engine represents a xorm engine or session. // Engine represents a xorm engine or session.
@ -70,167 +63,38 @@ type Engine interface {
// TableInfo returns table's information via an object // TableInfo returns table's information via an object
func TableInfo(v any) (*schemas.Table, error) { func TableInfo(v any) (*schemas.Table, error) {
return x.TableInfo(v) return xormEngine.TableInfo(v)
} }
// DumpTables dump tables information // RegisterModel registers model, if initFuncs provided, it will be invoked after data model sync
func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error {
return x.DumpTables(tables, w, tp...)
}
// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync
func RegisterModel(bean any, initFunc ...func() error) { func RegisterModel(bean any, initFunc ...func() error) {
tables = append(tables, bean) registeredModels = append(registeredModels, bean)
if len(initFuncs) > 0 && initFunc[0] != nil { if len(registeredInitFuncs) > 0 && initFunc[0] != nil {
initFuncs = append(initFuncs, initFunc[0]) registeredInitFuncs = append(registeredInitFuncs, initFunc[0])
} }
} }
func init() {
gonicNames := []string{"SSL", "UID"}
for _, name := range gonicNames {
names.LintGonicMapper[name] = true
}
}
// newXORMEngine returns a new XORM engine from the configuration
func newXORMEngine() (*xorm.Engine, error) {
connStr, err := setting.DBConnStr()
if err != nil {
return nil, err
}
var engine *xorm.Engine
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
// OK whilst we sort out our schema issues - create a schema aware postgres
registerPostgresSchemaDriver()
engine, err = xorm.NewEngine("postgresschema", connStr)
} else {
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
}
if err != nil {
return nil, err
}
if setting.Database.Type == "mysql" {
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
} else if setting.Database.Type == "mssql" {
engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"})
}
engine.SetSchema(setting.Database.Schema)
return engine, nil
}
// SyncAllTables sync the schemas of all tables, is required by unit test code // SyncAllTables sync the schemas of all tables, is required by unit test code
func SyncAllTables() error { func SyncAllTables() error {
_, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{ _, err := xormEngine.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
WarnIfDatabaseColumnMissed: true, WarnIfDatabaseColumnMissed: true,
}, tables...) }, registeredModels...)
return err return err
} }
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
func InitEngine(ctx context.Context) error {
xormEngine, err := newXORMEngine()
if err != nil {
if strings.Contains(err.Error(), "SQLite3 support") {
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
}
return fmt.Errorf("failed to connect to database: %w", err)
}
xormEngine.SetMapper(names.GonicMapper{})
// WARNING: for serv command, MUST remove the output to os.stdout,
// so use log file to instead print to stdout.
xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL))
xormEngine.ShowSQL(setting.Database.LogSQL)
xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns)
xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns)
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
}
// SetDefaultEngine sets the default engine for db
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
x = eng
DefaultContext = &Context{Context: ctx, engine: x}
}
// UnsetDefaultEngine closes and unsets the default engine
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `x` and DefaultContext without close
// Global database engine related functions are all racy and there is no graceful close right now.
func UnsetDefaultEngine() {
if x != nil {
_ = x.Close()
x = nil
}
DefaultContext = nil
}
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
// This function must never call .Sync() if the provided migration function fails.
// When called from the "doctor" command, the migration function is a version check
// that prevents the doctor from fixing anything in the database if the migration level
// is different from the expected value.
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
if err = InitEngine(ctx); err != nil {
return err
}
if err = x.Ping(); err != nil {
return err
}
preprocessDatabaseCollation(x)
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
//
// Installation should only be being re-run if users want to recover an old database.
// However, we should think carefully about should we support re-install on an installed instance,
// as there may be other problems due to secret reinitialization.
if err = migrateFunc(x); err != nil {
return fmt.Errorf("migrate: %w", err)
}
if err = SyncAllTables(); err != nil {
return fmt.Errorf("sync database struct error: %w", err)
}
for _, initFunc := range initFuncs {
if err := initFunc(); err != nil {
return fmt.Errorf("initFunc failed: %w", err)
}
}
return nil
}
// NamesToBean return a list of beans or an error // NamesToBean return a list of beans or an error
func NamesToBean(names ...string) ([]any, error) { func NamesToBean(names ...string) ([]any, error) {
beans := []any{} beans := []any{}
if len(names) == 0 { if len(names) == 0 {
beans = append(beans, tables...) beans = append(beans, registeredModels...)
return beans, nil return beans, nil
} }
// Need to map provided names to beans... // Need to map provided names to beans...
beanMap := make(map[string]any) beanMap := make(map[string]any)
for _, bean := range tables { for _, bean := range registeredModels {
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
beanMap[strings.ToLower(x.TableName(bean))] = bean beanMap[strings.ToLower(xormEngine.TableName(bean))] = bean
beanMap[strings.ToLower(x.TableName(bean, true))] = bean beanMap[strings.ToLower(xormEngine.TableName(bean, true))] = bean
} }
gotBean := make(map[any]bool) gotBean := make(map[any]bool)
@ -247,36 +111,9 @@ func NamesToBean(names ...string) ([]any, error) {
return beans, nil return beans, nil
} }
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
func DumpDatabase(filePath, dbType string) error {
var tbs []*schemas.Table
for _, t := range tables {
t, err := x.TableInfo(t)
if err != nil {
return err
}
tbs = append(tbs, t)
}
type Version struct {
ID int64 `xorm:"pk autoincr"`
Version int64
}
t, err := x.TableInfo(&Version{})
if err != nil {
return err
}
tbs = append(tbs, t)
if len(dbType) > 0 {
return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
}
return x.DumpTablesToFile(tbs, filePath)
}
// MaxBatchInsertSize returns the table's max batch insert size // MaxBatchInsertSize returns the table's max batch insert size
func MaxBatchInsertSize(bean any) int { func MaxBatchInsertSize(bean any) int {
t, err := x.TableInfo(bean) t, err := xormEngine.TableInfo(bean)
if err != nil { if err != nil {
return 50 return 50
} }
@ -285,18 +122,18 @@ func MaxBatchInsertSize(bean any) int {
// IsTableNotEmpty returns true if table has at least one record // IsTableNotEmpty returns true if table has at least one record
func IsTableNotEmpty(beanOrTableName any) (bool, error) { func IsTableNotEmpty(beanOrTableName any) (bool, error) {
return x.Table(beanOrTableName).Exist() return xormEngine.Table(beanOrTableName).Exist()
} }
// DeleteAllRecords will delete all the records of this table // DeleteAllRecords will delete all the records of this table
func DeleteAllRecords(tableName string) error { func DeleteAllRecords(tableName string) error {
_, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName)) _, err := xormEngine.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
return err return err
} }
// GetMaxID will return max id of the table // GetMaxID will return max id of the table
func GetMaxID(beanOrTableName any) (maxID int64, err error) { func GetMaxID(beanOrTableName any) (maxID int64, err error) {
_, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID) _, err = xormEngine.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
return maxID, err return maxID, err
} }
@ -308,24 +145,3 @@ func SetLogSQL(ctx context.Context, on bool) {
sess.Engine().ShowSQL(on) 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
}

33
models/db/engine_dump.go Normal file
View File

@ -0,0 +1,33 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import "xorm.io/xorm/schemas"
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
func DumpDatabase(filePath, dbType string) error {
var tbs []*schemas.Table
for _, t := range registeredModels {
t, err := xormEngine.TableInfo(t)
if err != nil {
return err
}
tbs = append(tbs, t)
}
type Version struct {
ID int64 `xorm:"pk autoincr"`
Version int64
}
t, err := xormEngine.TableInfo(&Version{})
if err != nil {
return err
}
tbs = append(tbs, t)
if dbType != "" {
return xormEngine.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
}
return xormEngine.DumpTablesToFile(tbs, filePath)
}

34
models/db/engine_hook.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import (
"context"
"time"
"code.gitea.io/gitea/modules/log"
"xorm.io/xorm/contexts"
)
type SlowQueryHook struct {
Threshold time.Duration
Logger log.Logger
}
var _ contexts.Hook = (*SlowQueryHook)(nil)
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
}

140
models/db/engine_init.go Normal file
View File

@ -0,0 +1,140 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package db
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm"
"xorm.io/xorm/names"
)
func init() {
gonicNames := []string{"SSL", "UID"}
for _, name := range gonicNames {
names.LintGonicMapper[name] = true
}
}
// newXORMEngine returns a new XORM engine from the configuration
func newXORMEngine() (*xorm.Engine, error) {
connStr, err := setting.DBConnStr()
if err != nil {
return nil, err
}
var engine *xorm.Engine
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
// OK whilst we sort out our schema issues - create a schema aware postgres
registerPostgresSchemaDriver()
engine, err = xorm.NewEngine("postgresschema", connStr)
} else {
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
}
if err != nil {
return nil, err
}
if setting.Database.Type == "mysql" {
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
} else if setting.Database.Type == "mssql" {
engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"})
}
engine.SetSchema(setting.Database.Schema)
return engine, nil
}
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
func InitEngine(ctx context.Context) error {
xe, err := newXORMEngine()
if err != nil {
if strings.Contains(err.Error(), "SQLite3 support") {
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
}
return fmt.Errorf("failed to connect to database: %w", err)
}
xe.SetMapper(names.GonicMapper{})
// WARNING: for serv command, MUST remove the output to os.stdout,
// so use log file to instead print to stdout.
xe.SetLogger(NewXORMLogger(setting.Database.LogSQL))
xe.ShowSQL(setting.Database.LogSQL)
xe.SetMaxOpenConns(setting.Database.MaxOpenConns)
xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
xe.SetDefaultContext(ctx)
if setting.Database.SlowQueryThreshold > 0 {
xe.AddHook(&SlowQueryHook{
Threshold: setting.Database.SlowQueryThreshold,
Logger: log.GetLogger("xorm"),
})
}
SetDefaultEngine(ctx, xe)
return nil
}
// SetDefaultEngine sets the default engine for db
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
xormEngine = eng
DefaultContext = &Context{Context: ctx, engine: xormEngine}
}
// UnsetDefaultEngine closes and unsets the default engine
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `xormEngine` and DefaultContext without close
// Global database engine related functions are all racy and there is no graceful close right now.
func UnsetDefaultEngine() {
if xormEngine != nil {
_ = xormEngine.Close()
xormEngine = nil
}
DefaultContext = nil
}
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
// This function must never call .Sync() if the provided migration function fails.
// When called from the "doctor" command, the migration function is a version check
// that prevents the doctor from fixing anything in the database if the migration level
// is different from the expected value.
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
if err = InitEngine(ctx); err != nil {
return err
}
if err = xormEngine.Ping(); err != nil {
return err
}
preprocessDatabaseCollation(xormEngine)
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
//
// Installation should only be being re-run if users want to recover an old database.
// However, we should think carefully about should we support re-install on an installed instance,
// as there may be other problems due to secret reinitialization.
if err = migrateFunc(xormEngine); err != nil {
return fmt.Errorf("migrate: %w", err)
}
if err = SyncAllTables(); err != nil {
return fmt.Errorf("sync database struct error: %w", err)
}
for _, initFunc := range registeredInitFuncs {
if err := initFunc(); err != nil {
return fmt.Errorf("initFunc failed: %w", err)
}
}
return nil
}

View File

@ -17,11 +17,11 @@ func CountBadSequences(_ context.Context) (int64, error) {
return 0, nil return 0, nil
} }
sess := x.NewSession() sess := xormEngine.NewSession()
defer sess.Close() defer sess.Close()
var sequences []string var sequences []string
schema := x.Dialect().URI().Schema schema := xormEngine.Dialect().URI().Schema
sess.Engine().SetSchema("") sess.Engine().SetSchema("")
if err := sess.Table("information_schema.sequences").Cols("sequence_name").Where("sequence_name LIKE 'tmp_recreate__%_id_seq%' AND sequence_catalog = ?", setting.Database.Name).Find(&sequences); err != nil { if err := sess.Table("information_schema.sequences").Cols("sequence_name").Where("sequence_name LIKE 'tmp_recreate__%_id_seq%' AND sequence_catalog = ?", setting.Database.Name).Find(&sequences); err != nil {
@ -38,7 +38,7 @@ func FixBadSequences(_ context.Context) error {
return nil return nil
} }
sess := x.NewSession() sess := xormEngine.NewSession()
defer sess.Close() defer sess.Close()
if err := sess.Begin(); err != nil { if err := sess.Begin(); err != nil {
return err return err

View File

@ -1,552 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package models
import (
"fmt"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
)
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
type ErrUserOwnRepos struct {
UID int64
}
// IsErrUserOwnRepos checks if an error is a ErrUserOwnRepos.
func IsErrUserOwnRepos(err error) bool {
_, ok := err.(ErrUserOwnRepos)
return ok
}
func (err ErrUserOwnRepos) Error() string {
return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
}
// ErrUserHasOrgs represents a "UserHasOrgs" kind of error.
type ErrUserHasOrgs struct {
UID int64
}
// IsErrUserHasOrgs checks if an error is a ErrUserHasOrgs.
func IsErrUserHasOrgs(err error) bool {
_, ok := err.(ErrUserHasOrgs)
return ok
}
func (err ErrUserHasOrgs) Error() string {
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
}
// ErrUserOwnPackages notifies that the user (still) owns the packages.
type ErrUserOwnPackages struct {
UID int64
}
// IsErrUserOwnPackages checks if an error is an ErrUserOwnPackages.
func IsErrUserOwnPackages(err error) bool {
_, ok := err.(ErrUserOwnPackages)
return ok
}
func (err ErrUserOwnPackages) Error() string {
return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID)
}
// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error.
type ErrDeleteLastAdminUser struct {
UID int64
}
// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser.
func IsErrDeleteLastAdminUser(err error) bool {
_, ok := err.(ErrDeleteLastAdminUser)
return ok
}
func (err ErrDeleteLastAdminUser) Error() string {
return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID)
}
// ErrNoPendingRepoTransfer is an error type for repositories without a pending
// transfer request
type ErrNoPendingRepoTransfer struct {
RepoID int64
}
func (err ErrNoPendingRepoTransfer) Error() string {
return fmt.Sprintf("repository doesn't have a pending transfer [repo_id: %d]", err.RepoID)
}
// IsErrNoPendingTransfer is an error type when a repository has no pending
// transfers
func IsErrNoPendingTransfer(err error) bool {
_, ok := err.(ErrNoPendingRepoTransfer)
return ok
}
func (err ErrNoPendingRepoTransfer) Unwrap() error {
return util.ErrNotExist
}
// ErrRepoTransferInProgress represents the state of a repository that has an
// ongoing transfer
type ErrRepoTransferInProgress struct {
Uname string
Name string
}
// IsErrRepoTransferInProgress checks if an error is a ErrRepoTransferInProgress.
func IsErrRepoTransferInProgress(err error) bool {
_, ok := err.(ErrRepoTransferInProgress)
return ok
}
func (err ErrRepoTransferInProgress) Error() string {
return fmt.Sprintf("repository is already being transferred [uname: %s, name: %s]", err.Uname, err.Name)
}
func (err ErrRepoTransferInProgress) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error.
type ErrInvalidCloneAddr struct {
Host string
IsURLError bool
IsInvalidPath bool
IsProtocolInvalid bool
IsPermissionDenied bool
LocalPath bool
}
// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr.
func IsErrInvalidCloneAddr(err error) bool {
_, ok := err.(*ErrInvalidCloneAddr)
return ok
}
func (err *ErrInvalidCloneAddr) Error() string {
if err.IsInvalidPath {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided path is invalid", err.Host)
}
if err.IsProtocolInvalid {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url protocol is not allowed", err.Host)
}
if err.IsPermissionDenied {
return fmt.Sprintf("migration/cloning from '%s' is not allowed.", err.Host)
}
if err.IsURLError {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url is invalid", err.Host)
}
return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host)
}
func (err *ErrInvalidCloneAddr) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrUpdateTaskNotExist represents a "UpdateTaskNotExist" kind of error.
type ErrUpdateTaskNotExist struct {
UUID string
}
// IsErrUpdateTaskNotExist checks if an error is a ErrUpdateTaskNotExist.
func IsErrUpdateTaskNotExist(err error) bool {
_, ok := err.(ErrUpdateTaskNotExist)
return ok
}
func (err ErrUpdateTaskNotExist) Error() string {
return fmt.Sprintf("update task does not exist [uuid: %s]", err.UUID)
}
func (err ErrUpdateTaskNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrInvalidTagName represents a "InvalidTagName" kind of error.
type ErrInvalidTagName struct {
TagName string
}
// IsErrInvalidTagName checks if an error is a ErrInvalidTagName.
func IsErrInvalidTagName(err error) bool {
_, ok := err.(ErrInvalidTagName)
return ok
}
func (err ErrInvalidTagName) Error() string {
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
}
func (err ErrInvalidTagName) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrProtectedTagName represents a "ProtectedTagName" kind of error.
type ErrProtectedTagName struct {
TagName string
}
// IsErrProtectedTagName checks if an error is a ErrProtectedTagName.
func IsErrProtectedTagName(err error) bool {
_, ok := err.(ErrProtectedTagName)
return ok
}
func (err ErrProtectedTagName) Error() string {
return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName)
}
func (err ErrProtectedTagName) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
type ErrRepoFileAlreadyExists struct {
Path string
}
// IsErrRepoFileAlreadyExists checks if an error is a ErrRepoFileAlreadyExists.
func IsErrRepoFileAlreadyExists(err error) bool {
_, ok := err.(ErrRepoFileAlreadyExists)
return ok
}
func (err ErrRepoFileAlreadyExists) Error() string {
return fmt.Sprintf("repository file already exists [path: %s]", err.Path)
}
func (err ErrRepoFileAlreadyExists) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrRepoFileDoesNotExist represents a "RepoFileDoesNotExist" kind of error.
type ErrRepoFileDoesNotExist struct {
Path string
Name string
}
// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist.
func IsErrRepoFileDoesNotExist(err error) bool {
_, ok := err.(ErrRepoFileDoesNotExist)
return ok
}
func (err ErrRepoFileDoesNotExist) Error() string {
return fmt.Sprintf("repository file does not exist [path: %s]", err.Path)
}
func (err ErrRepoFileDoesNotExist) Unwrap() error {
return util.ErrNotExist
}
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
type ErrFilenameInvalid struct {
Path string
}
// IsErrFilenameInvalid checks if an error is an ErrFilenameInvalid.
func IsErrFilenameInvalid(err error) bool {
_, ok := err.(ErrFilenameInvalid)
return ok
}
func (err ErrFilenameInvalid) Error() string {
return fmt.Sprintf("path contains a malformed path component [path: %s]", err.Path)
}
func (err ErrFilenameInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrUserCannotCommit represents "UserCannotCommit" kind of error.
type ErrUserCannotCommit struct {
UserName string
}
// IsErrUserCannotCommit checks if an error is an ErrUserCannotCommit.
func IsErrUserCannotCommit(err error) bool {
_, ok := err.(ErrUserCannotCommit)
return ok
}
func (err ErrUserCannotCommit) Error() string {
return fmt.Sprintf("user cannot commit to repo [user: %s]", err.UserName)
}
func (err ErrUserCannotCommit) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrFilePathInvalid represents a "FilePathInvalid" kind of error.
type ErrFilePathInvalid struct {
Message string
Path string
Name string
Type git.EntryMode
}
// IsErrFilePathInvalid checks if an error is an ErrFilePathInvalid.
func IsErrFilePathInvalid(err error) bool {
_, ok := err.(ErrFilePathInvalid)
return ok
}
func (err ErrFilePathInvalid) Error() string {
if err.Message != "" {
return err.Message
}
return fmt.Sprintf("path is invalid [path: %s]", err.Path)
}
func (err ErrFilePathInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrFilePathProtected represents a "FilePathProtected" kind of error.
type ErrFilePathProtected struct {
Message string
Path string
}
// IsErrFilePathProtected checks if an error is an ErrFilePathProtected.
func IsErrFilePathProtected(err error) bool {
_, ok := err.(ErrFilePathProtected)
return ok
}
func (err ErrFilePathProtected) Error() string {
if err.Message != "" {
return err.Message
}
return fmt.Sprintf("path is protected and can not be changed [path: %s]", err.Path)
}
func (err ErrFilePathProtected) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrDisallowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
type ErrDisallowedToMerge struct {
Reason string
}
// IsErrDisallowedToMerge checks if an error is an ErrDisallowedToMerge.
func IsErrDisallowedToMerge(err error) bool {
_, ok := err.(ErrDisallowedToMerge)
return ok
}
func (err ErrDisallowedToMerge) Error() string {
return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
}
func (err ErrDisallowedToMerge) Unwrap() error {
return util.ErrPermissionDenied
}
// ErrTagAlreadyExists represents an error that tag with such name already exists.
type ErrTagAlreadyExists struct {
TagName string
}
// IsErrTagAlreadyExists checks if an error is an ErrTagAlreadyExists.
func IsErrTagAlreadyExists(err error) bool {
_, ok := err.(ErrTagAlreadyExists)
return ok
}
func (err ErrTagAlreadyExists) Error() string {
return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
}
func (err ErrTagAlreadyExists) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
type ErrSHADoesNotMatch struct {
Path string
GivenSHA string
CurrentSHA string
}
// IsErrSHADoesNotMatch checks if an error is a ErrSHADoesNotMatch.
func IsErrSHADoesNotMatch(err error) bool {
_, ok := err.(ErrSHADoesNotMatch)
return ok
}
func (err ErrSHADoesNotMatch) Error() string {
return fmt.Sprintf("sha does not match [given: %s, expected: %s]", err.GivenSHA, err.CurrentSHA)
}
// ErrSHANotFound represents a "SHADoesNotMatch" kind of error.
type ErrSHANotFound struct {
SHA string
}
// IsErrSHANotFound checks if an error is a ErrSHANotFound.
func IsErrSHANotFound(err error) bool {
_, ok := err.(ErrSHANotFound)
return ok
}
func (err ErrSHANotFound) Error() string {
return fmt.Sprintf("sha not found [%s]", err.SHA)
}
func (err ErrSHANotFound) Unwrap() error {
return util.ErrNotExist
}
// ErrCommitIDDoesNotMatch represents a "CommitIDDoesNotMatch" kind of error.
type ErrCommitIDDoesNotMatch struct {
GivenCommitID string
CurrentCommitID string
}
// IsErrCommitIDDoesNotMatch checks if an error is a ErrCommitIDDoesNotMatch.
func IsErrCommitIDDoesNotMatch(err error) bool {
_, ok := err.(ErrCommitIDDoesNotMatch)
return ok
}
func (err ErrCommitIDDoesNotMatch) Error() string {
return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
}
// ErrSHAOrCommitIDNotProvided represents a "SHAOrCommitIDNotProvided" kind of error.
type ErrSHAOrCommitIDNotProvided struct{}
// IsErrSHAOrCommitIDNotProvided checks if an error is a ErrSHAOrCommitIDNotProvided.
func IsErrSHAOrCommitIDNotProvided(err error) bool {
_, ok := err.(ErrSHAOrCommitIDNotProvided)
return ok
}
func (err ErrSHAOrCommitIDNotProvided) Error() string {
return "a SHA or commit ID must be proved when updating a file"
}
// ErrInvalidMergeStyle represents an error if merging with disabled merge strategy
type ErrInvalidMergeStyle struct {
ID int64
Style repo_model.MergeStyle
}
// IsErrInvalidMergeStyle checks if an error is a ErrInvalidMergeStyle.
func IsErrInvalidMergeStyle(err error) bool {
_, ok := err.(ErrInvalidMergeStyle)
return ok
}
func (err ErrInvalidMergeStyle) Error() string {
return fmt.Sprintf("merge strategy is not allowed or is invalid [repo_id: %d, strategy: %s]",
err.ID, err.Style)
}
func (err ErrInvalidMergeStyle) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrMergeConflicts represents an error if merging fails with a conflict
type ErrMergeConflicts struct {
Style repo_model.MergeStyle
StdOut string
StdErr string
Err error
}
// IsErrMergeConflicts checks if an error is a ErrMergeConflicts.
func IsErrMergeConflicts(err error) bool {
_, ok := err.(ErrMergeConflicts)
return ok
}
func (err ErrMergeConflicts) Error() string {
return fmt.Sprintf("Merge Conflict Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// ErrMergeUnrelatedHistories represents an error if merging fails due to unrelated histories
type ErrMergeUnrelatedHistories struct {
Style repo_model.MergeStyle
StdOut string
StdErr string
Err error
}
// IsErrMergeUnrelatedHistories checks if an error is a ErrMergeUnrelatedHistories.
func IsErrMergeUnrelatedHistories(err error) bool {
_, ok := err.(ErrMergeUnrelatedHistories)
return ok
}
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
CommitSHA string
StdOut string
StdErr string
Err error
}
// IsErrRebaseConflicts checks if an error is a ErrRebaseConflicts.
func IsErrRebaseConflicts(err error) bool {
_, ok := err.(ErrRebaseConflicts)
return ok
}
func (err ErrRebaseConflicts) Error() string {
return fmt.Sprintf("Rebase Error: %v: Whilst Rebasing: %s\n%s\n%s", err.Err, err.CommitSHA, err.StdErr, err.StdOut)
}
// ErrPullRequestHasMerged represents a "PullRequestHasMerged"-error
type ErrPullRequestHasMerged struct {
ID int64
IssueID int64
HeadRepoID int64
BaseRepoID int64
HeadBranch string
BaseBranch string
}
// IsErrPullRequestHasMerged checks if an error is a ErrPullRequestHasMerged.
func IsErrPullRequestHasMerged(err error) bool {
_, ok := err.(ErrPullRequestHasMerged)
return ok
}
// Error does pretty-printing :D
func (err ErrPullRequestHasMerged) Error() string {
return fmt.Sprintf("pull request has merged [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
}

View File

@ -13,9 +13,9 @@ import (
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/testlogger" "code.gitea.io/gitea/modules/testlogger"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -92,10 +92,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
func MainTest(m *testing.M) { func MainTest(m *testing.M) {
testlogger.Init() testlogger.Init()
giteaRoot := base.SetupGiteaRoot() giteaRoot := test.SetupGiteaRoot()
if giteaRoot == "" {
testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n")
}
giteaBinary := "gitea" giteaBinary := "gitea"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
giteaBinary += ".exe" giteaBinary += ".exe"

View File

@ -9,11 +9,8 @@ import (
"fmt" "fmt"
"strings" "strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -407,33 +404,6 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
return u, nil return u, nil
} }
// DeleteOrganization deletes models associated to an organization.
func DeleteOrganization(ctx context.Context, org *Organization) error {
if org.Type != user_model.UserTypeOrganization {
return fmt.Errorf("%s is a user not an organization", org.Name)
}
if err := db.DeleteBeans(ctx,
&Team{OrgID: org.ID},
&OrgUser{OrgID: org.ID},
&TeamUser{OrgID: org.ID},
&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)
}
if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil {
return fmt.Errorf("Delete: %w", err)
}
return nil
}
// GetOrgUserMaxAuthorizeLevel returns highest authorize level of user in an organization // GetOrgUserMaxAuthorizeLevel returns highest authorize level of user in an organization
func (org *Organization) GetOrgUserMaxAuthorizeLevel(ctx context.Context, uid int64) (perm.AccessMode, error) { func (org *Organization) GetOrgUserMaxAuthorizeLevel(ctx context.Context, uid int64) (perm.AccessMode, error) {
var authorize perm.AccessMode var authorize perm.AccessMode
@ -604,7 +574,9 @@ func RemoveOrgRepo(ctx context.Context, orgID, repoID int64) error {
return err return err
} }
func (org *Organization) getUserTeams(ctx context.Context, userID int64, cols ...string) ([]*Team, error) { // GetUserTeams returns all teams that belong to user,
// and that the user has joined.
func (org *Organization) GetUserTeams(ctx context.Context, userID int64, cols ...string) ([]*Team, error) {
teams := make([]*Team, 0, org.NumTeams) teams := make([]*Team, 0, org.NumTeams)
return teams, db.GetEngine(ctx). return teams, db.GetEngine(ctx).
Where("`team_user`.org_id = ?", org.ID). Where("`team_user`.org_id = ?", org.ID).
@ -616,7 +588,8 @@ func (org *Organization) getUserTeams(ctx context.Context, userID int64, cols ..
Find(&teams) Find(&teams)
} }
func (org *Organization) getUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) { // GetUserTeamIDs returns of all team IDs of the organization that user is member of.
func (org *Organization) GetUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) {
teamIDs := make([]int64, 0, org.NumTeams) teamIDs := make([]int64, 0, org.NumTeams)
return teamIDs, db.GetEngine(ctx). return teamIDs, db.GetEngine(ctx).
Table("team"). Table("team").
@ -640,175 +613,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) { func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode) return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
} }
// GetUserTeamIDs returns of all team IDs of the organization that user is member of.
func (org *Organization) GetUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) {
return org.getUserTeamIDs(ctx, userID)
}
// GetUserTeams returns all teams that belong to user,
// and that the user has joined.
func (org *Organization) GetUserTeams(ctx context.Context, userID int64) ([]*Team, error) {
return org.getUserTeams(ctx, userID)
}
// AccessibleReposEnvironment operations involving the repositories that are
// accessible to a particular user
type AccessibleReposEnvironment interface {
CountRepos() (int64, error)
RepoIDs(page, pageSize int) ([]int64, error)
Repos(page, pageSize int) (repo_model.RepositoryList, error)
MirrorRepos() (repo_model.RepositoryList, error)
AddKeyword(keyword string)
SetSort(db.SearchOrderBy)
}
type accessibleReposEnv struct {
org *Organization
user *user_model.User
team *Team
teamIDs []int64
ctx context.Context
keyword string
orderBy db.SearchOrderBy
}
// AccessibleReposEnv builds an AccessibleReposEnvironment for the repositories in `org`
// that are accessible to the specified user.
func AccessibleReposEnv(ctx context.Context, org *Organization, userID int64) (AccessibleReposEnvironment, error) {
var user *user_model.User
if userID > 0 {
u, err := user_model.GetUserByID(ctx, userID)
if err != nil {
return nil, err
}
user = u
}
teamIDs, err := org.getUserTeamIDs(ctx, userID)
if err != nil {
return nil, err
}
return &accessibleReposEnv{
org: org,
user: user,
teamIDs: teamIDs,
ctx: ctx,
orderBy: db.SearchOrderByRecentUpdated,
}, nil
}
// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org`
// that are accessible to the specified team.
func (org *Organization) AccessibleTeamReposEnv(ctx context.Context, team *Team) AccessibleReposEnvironment {
return &accessibleReposEnv{
org: org,
team: team,
ctx: ctx,
orderBy: db.SearchOrderByRecentUpdated,
}
}
func (env *accessibleReposEnv) cond() builder.Cond {
cond := builder.NewCond()
if env.team != nil {
cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID})
} else {
if env.user == nil || !env.user.IsRestricted {
cond = cond.Or(builder.Eq{
"`repository`.owner_id": env.org.ID,
"`repository`.is_private": false,
})
}
if len(env.teamIDs) > 0 {
cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs))
}
}
if env.keyword != "" {
cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)})
}
return cond
}
func (env *accessibleReposEnv) CountRepos() (int64, error) {
repoCount, err := db.GetEngine(env.ctx).
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
Where(env.cond()).
Distinct("`repository`.id").
Count(&repo_model.Repository{})
if err != nil {
return 0, fmt.Errorf("count user repositories in organization: %w", err)
}
return repoCount, nil
}
func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) {
if page <= 0 {
page = 1
}
repoIDs := make([]int64, 0, pageSize)
return repoIDs, db.GetEngine(env.ctx).
Table("repository").
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
Where(env.cond()).
GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]).
OrderBy(string(env.orderBy)).
Limit(pageSize, (page-1)*pageSize).
Cols("`repository`.id").
Find(&repoIDs)
}
func (env *accessibleReposEnv) Repos(page, pageSize int) (repo_model.RepositoryList, error) {
repoIDs, err := env.RepoIDs(page, pageSize)
if err != nil {
return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err)
}
repos := make([]*repo_model.Repository, 0, len(repoIDs))
if len(repoIDs) == 0 {
return repos, nil
}
return repos, db.GetEngine(env.ctx).
In("`repository`.id", repoIDs).
OrderBy(string(env.orderBy)).
Find(&repos)
}
func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) {
repoIDs := make([]int64, 0, 10)
return repoIDs, db.GetEngine(env.ctx).
Table("repository").
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id AND `repository`.is_mirror=?", true).
Where(env.cond()).
GroupBy("`repository`.id, `repository`.updated_unix").
OrderBy(string(env.orderBy)).
Cols("`repository`.id").
Find(&repoIDs)
}
func (env *accessibleReposEnv) MirrorRepos() (repo_model.RepositoryList, error) {
repoIDs, err := env.MirrorRepoIDs()
if err != nil {
return nil, fmt.Errorf("MirrorRepoIDs: %w", err)
}
repos := make([]*repo_model.Repository, 0, len(repoIDs))
if len(repoIDs) == 0 {
return repos, nil
}
return repos, db.GetEngine(env.ctx).
In("`repository`.id", repoIDs).
Find(&repos)
}
func (env *accessibleReposEnv) AddKeyword(keyword string) {
env.keyword = keyword
}
func (env *accessibleReposEnv) SetSort(orderBy db.SearchOrderBy) {
env.orderBy = orderBy
}

View File

@ -1,17 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package organization
import (
"context"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
)
// GetOrgRepositories get repos belonging to the given organization
func GetOrgRepositories(ctx context.Context, orgID int64) (repo_model.RepositoryList, error) {
var orgRepos []*repo_model.Repository
return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos)
}

View File

@ -318,7 +318,7 @@ func TestAccessibleReposEnv_CountRepos(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
testSuccess := func(userID, expectedCount int64) { testSuccess := func(userID, expectedCount int64) {
env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
assert.NoError(t, err) assert.NoError(t, err)
count, err := env.CountRepos() count, err := env.CountRepos()
assert.NoError(t, err) assert.NoError(t, err)
@ -332,7 +332,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
testSuccess := func(userID int64, expectedRepoIDs []int64) { testSuccess := func(userID int64, expectedRepoIDs []int64) {
env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
assert.NoError(t, err) assert.NoError(t, err)
repoIDs, err := env.RepoIDs(1, 100) repoIDs, err := env.RepoIDs(1, 100)
assert.NoError(t, err) assert.NoError(t, err)
@ -346,7 +346,7 @@ func TestAccessibleReposEnv_Repos(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
testSuccess := func(userID int64, expectedRepoIDs []int64) { testSuccess := func(userID int64, expectedRepoIDs []int64) {
env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
assert.NoError(t, err) assert.NoError(t, err)
repos, err := env.Repos(1, 100) repos, err := env.Repos(1, 100)
assert.NoError(t, err) assert.NoError(t, err)
@ -365,7 +365,7 @@ func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
testSuccess := func(userID int64, expectedRepoIDs []int64) { testSuccess := func(userID int64, expectedRepoIDs []int64) {
env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
assert.NoError(t, err) assert.NoError(t, err)
repos, err := env.MirrorRepos() repos, err := env.MirrorRepos()
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -36,6 +36,21 @@ func init() {
db.RegisterModel(new(OrgUser)) db.RegisterModel(new(OrgUser))
} }
// ErrUserHasOrgs represents a "UserHasOrgs" kind of error.
type ErrUserHasOrgs struct {
UID int64
}
// IsErrUserHasOrgs checks if an error is a ErrUserHasOrgs.
func IsErrUserHasOrgs(err error) bool {
_, ok := err.(ErrUserHasOrgs)
return ok
}
func (err ErrUserHasOrgs) Error() string {
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
}
// GetOrganizationCount returns count of membership of organization of the user. // GetOrganizationCount returns count of membership of organization of the user.
func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error) { func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error) {
return db.GetEngine(ctx). return db.GetEngine(ctx).

View File

@ -11,7 +11,6 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -79,7 +78,6 @@ type Team struct {
Name string Name string
Description string Description string
AccessMode perm.AccessMode `xorm:"'authorize'"` AccessMode perm.AccessMode `xorm:"'authorize'"`
Repos []*repo_model.Repository `xorm:"-"`
Members []*user_model.User `xorm:"-"` Members []*user_model.User `xorm:"-"`
NumRepos int NumRepos int
NumMembers int NumMembers int
@ -155,17 +153,6 @@ func (t *Team) IsMember(ctx context.Context, userID int64) bool {
return isMember return isMember
} }
// LoadRepositories returns paginated repositories in team of organization.
func (t *Team) LoadRepositories(ctx context.Context) (err error) {
if t.Repos != nil {
return nil
}
t.Repos, err = GetTeamRepositories(ctx, &SearchTeamRepoOptions{
TeamID: t.ID,
})
return err
}
// LoadMembers returns paginated members in team of organization. // LoadMembers returns paginated members in team of organization.
func (t *Team) LoadMembers(ctx context.Context) (err error) { func (t *Team) LoadMembers(ctx context.Context) (err error) {
t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{ t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{

View File

@ -9,7 +9,6 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"xorm.io/builder" "xorm.io/builder"
@ -98,11 +97,11 @@ func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64,
} }
// GetRepoTeams gets the list of teams that has access to the repository // GetRepoTeams gets the list of teams that has access to the repository
func GetRepoTeams(ctx context.Context, repo *repo_model.Repository) (teams TeamList, err error) { func GetRepoTeams(ctx context.Context, orgID, repoID int64) (teams TeamList, err error) {
return teams, db.GetEngine(ctx). return teams, db.GetEngine(ctx).
Join("INNER", "team_repo", "team_repo.team_id = team.id"). Join("INNER", "team_repo", "team_repo.team_id = team.id").
Where("team.org_id = ?", repo.OwnerID). Where("team.org_id = ?", orgID).
And("team_repo.repo_id=?", repo.ID). And("team_repo.repo_id=?", repoID).
OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END"). OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END").
Find(&teams) Find(&teams)
} }

View File

@ -8,10 +8,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"xorm.io/builder"
) )
// TeamRepo represents an team-repository relation. // TeamRepo represents an team-repository relation.
@ -32,29 +29,6 @@ func HasTeamRepo(ctx context.Context, orgID, teamID, repoID int64) bool {
return has return has
} }
type SearchTeamRepoOptions struct {
db.ListOptions
TeamID int64
}
// GetRepositories returns paginated repositories in team of organization.
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (repo_model.RepositoryList, error) {
sess := db.GetEngine(ctx)
if opts.TeamID > 0 {
sess = sess.In("id",
builder.Select("repo_id").
From("team_repo").
Where(builder.Eq{"team_id": opts.TeamID}),
)
}
if opts.PageSize > 0 {
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
}
var repos []*repo_model.Repository
return repos, sess.OrderBy("repository.name").
Find(&repos)
}
// AddTeamRepo adds a repo for an organization's team // AddTeamRepo adds a repo for an organization's team
func AddTeamRepo(ctx context.Context, orgID, teamID, repoID int64) error { func AddTeamRepo(ctx context.Context, orgID, teamID, repoID int64) error {
_, err := db.GetEngine(ctx).Insert(&TeamRepo{ _, err := db.GetEngine(ctx).Insert(&TeamRepo{

View File

@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -42,9 +43,12 @@ func TestTeam_GetRepositories(t *testing.T) {
test := func(teamID int64) { test := func(teamID int64) {
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
assert.NoError(t, team.LoadRepositories(db.DefaultContext)) repos, err := repo_model.GetTeamRepositories(db.DefaultContext, &repo_model.SearchTeamRepoOptions{
assert.Len(t, team.Repos, team.NumRepos) TeamID: team.ID,
for _, repo := range team.Repos { })
assert.NoError(t, err)
assert.Len(t, repos, team.NumRepos)
for _, repo := range repos {
unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repo.ID}) unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repo.ID})
} }
} }

View File

@ -301,6 +301,21 @@ func FindUnreferencedPackages(ctx context.Context) ([]*Package, error) {
Find(&ps) Find(&ps)
} }
// ErrUserOwnPackages notifies that the user (still) owns the packages.
type ErrUserOwnPackages struct {
UID int64
}
// IsErrUserOwnPackages checks if an error is an ErrUserOwnPackages.
func IsErrUserOwnPackages(err error) bool {
_, ok := err.(ErrUserOwnPackages)
return ok
}
func (err ErrUserOwnPackages) Error() string {
return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID)
}
// HasOwnerPackages tests if a user/org has accessible packages // HasOwnerPackages tests if a user/org has accessible packages
func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) { func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) {
return db.GetEngine(ctx). return db.GetEngine(ctx).

View File

@ -6,15 +6,12 @@ package models
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
_ "image/jpeg" // Needed for jpeg support _ "image/jpeg" // Needed for jpeg support
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_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/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -315,48 +312,3 @@ func DoctorUserStarNum(ctx context.Context) (err error) {
return err return err
} }
// DeleteDeployKey delete deploy keys
func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error {
key, err := asymkey_model.GetDeployKeyByID(ctx, id)
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
return nil
}
return fmt.Errorf("GetDeployKeyByID: %w", err)
}
// Check if user has access to delete this key.
if !doer.IsAdmin {
repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID)
if err != nil {
return fmt.Errorf("GetRepositoryByID: %w", err)
}
has, err := access_model.IsUserRepoAdmin(ctx, repo, doer)
if err != nil {
return fmt.Errorf("GetUserRepoPermission: %w", err)
} else if !has {
return asymkey_model.ErrKeyAccessDenied{
UserID: doer.ID,
KeyID: key.ID,
Note: "deploy",
}
}
}
if _, err := db.DeleteByID[asymkey_model.DeployKey](ctx, key.ID); err != nil {
return fmt.Errorf("delete deploy key [%d]: %w", key.ID, err)
}
// Check if this is the last reference to same key content.
has, err := asymkey_model.IsDeployKeyExistByKeyID(ctx, key.KeyID)
if err != nil {
return err
} else if !has {
if _, err = db.DeleteByID[asymkey_model.PublicKey](ctx, key.KeyID); err != nil {
return err
}
}
return nil
}

206
models/repo/org_repo.go Normal file
View File

@ -0,0 +1,206 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"xorm.io/builder"
)
// GetOrgRepositories get repos belonging to the given organization
func GetOrgRepositories(ctx context.Context, orgID int64) (RepositoryList, error) {
var orgRepos []*Repository
return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos)
}
type SearchTeamRepoOptions struct {
db.ListOptions
TeamID int64
}
// GetRepositories returns paginated repositories in team of organization.
func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) {
sess := db.GetEngine(ctx)
if opts.TeamID > 0 {
sess = sess.In("id",
builder.Select("repo_id").
From("team_repo").
Where(builder.Eq{"team_id": opts.TeamID}),
)
}
if opts.PageSize > 0 {
sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
}
var repos []*Repository
return repos, sess.OrderBy("repository.name").
Find(&repos)
}
// AccessibleReposEnvironment operations involving the repositories that are
// accessible to a particular user
type AccessibleReposEnvironment interface {
CountRepos() (int64, error)
RepoIDs(page, pageSize int) ([]int64, error)
Repos(page, pageSize int) (RepositoryList, error)
MirrorRepos() (RepositoryList, error)
AddKeyword(keyword string)
SetSort(db.SearchOrderBy)
}
type accessibleReposEnv struct {
org *org_model.Organization
user *user_model.User
team *org_model.Team
teamIDs []int64
ctx context.Context
keyword string
orderBy db.SearchOrderBy
}
// AccessibleReposEnv builds an AccessibleReposEnvironment for the repositories in `org`
// that are accessible to the specified user.
func AccessibleReposEnv(ctx context.Context, org *org_model.Organization, userID int64) (AccessibleReposEnvironment, error) {
var user *user_model.User
if userID > 0 {
u, err := user_model.GetUserByID(ctx, userID)
if err != nil {
return nil, err
}
user = u
}
teamIDs, err := org.GetUserTeamIDs(ctx, userID)
if err != nil {
return nil, err
}
return &accessibleReposEnv{
org: org,
user: user,
teamIDs: teamIDs,
ctx: ctx,
orderBy: db.SearchOrderByRecentUpdated,
}, nil
}
// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org`
// that are accessible to the specified team.
func AccessibleTeamReposEnv(ctx context.Context, org *org_model.Organization, team *org_model.Team) AccessibleReposEnvironment {
return &accessibleReposEnv{
org: org,
team: team,
ctx: ctx,
orderBy: db.SearchOrderByRecentUpdated,
}
}
func (env *accessibleReposEnv) cond() builder.Cond {
cond := builder.NewCond()
if env.team != nil {
cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID})
} else {
if env.user == nil || !env.user.IsRestricted {
cond = cond.Or(builder.Eq{
"`repository`.owner_id": env.org.ID,
"`repository`.is_private": false,
})
}
if len(env.teamIDs) > 0 {
cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs))
}
}
if env.keyword != "" {
cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)})
}
return cond
}
func (env *accessibleReposEnv) CountRepos() (int64, error) {
repoCount, err := db.GetEngine(env.ctx).
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
Where(env.cond()).
Distinct("`repository`.id").
Count(&Repository{})
if err != nil {
return 0, fmt.Errorf("count user repositories in organization: %w", err)
}
return repoCount, nil
}
func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) {
if page <= 0 {
page = 1
}
repoIDs := make([]int64, 0, pageSize)
return repoIDs, db.GetEngine(env.ctx).
Table("repository").
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
Where(env.cond()).
GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]).
OrderBy(string(env.orderBy)).
Limit(pageSize, (page-1)*pageSize).
Cols("`repository`.id").
Find(&repoIDs)
}
func (env *accessibleReposEnv) Repos(page, pageSize int) (RepositoryList, error) {
repoIDs, err := env.RepoIDs(page, pageSize)
if err != nil {
return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err)
}
repos := make([]*Repository, 0, len(repoIDs))
if len(repoIDs) == 0 {
return repos, nil
}
return repos, db.GetEngine(env.ctx).
In("`repository`.id", repoIDs).
OrderBy(string(env.orderBy)).
Find(&repos)
}
func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) {
repoIDs := make([]int64, 0, 10)
return repoIDs, db.GetEngine(env.ctx).
Table("repository").
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id AND `repository`.is_mirror=?", true).
Where(env.cond()).
GroupBy("`repository`.id, `repository`.updated_unix").
OrderBy(string(env.orderBy)).
Cols("`repository`.id").
Find(&repoIDs)
}
func (env *accessibleReposEnv) MirrorRepos() (RepositoryList, error) {
repoIDs, err := env.MirrorRepoIDs()
if err != nil {
return nil, fmt.Errorf("MirrorRepoIDs: %w", err)
}
repos := make([]*Repository, 0, len(repoIDs))
if len(repoIDs) == 0 {
return repos, nil
}
return repos, db.GetEngine(env.ctx).
In("`repository`.id", repoIDs).
Find(&repos)
}
func (env *accessibleReposEnv) AddKeyword(keyword string) {
env.keyword = keyword
}
func (env *accessibleReposEnv) SetSort(orderBy db.SearchOrderBy) {
env.orderBy = orderBy
}

View File

@ -37,7 +37,7 @@ type ErrUserDoesNotHaveAccessToRepo struct {
RepoName string RepoName string
} }
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists. // IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrUserDoesNotHaveAccessToRepo.
func IsErrUserDoesNotHaveAccessToRepo(err error) bool { func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
_, ok := err.(ErrUserDoesNotHaveAccessToRepo) _, ok := err.(ErrUserDoesNotHaveAccessToRepo)
return ok return ok
@ -866,6 +866,21 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
return repo return repo
} }
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
type ErrUserOwnRepos struct {
UID int64
}
// IsErrUserOwnRepos checks if an error is a ErrUserOwnRepos.
func IsErrUserOwnRepos(err error) bool {
_, ok := err.(ErrUserOwnRepos)
return ok
}
func (err ErrUserOwnRepos) Error() string {
return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
}
type CountRepositoryOptions struct { type CountRepositoryOptions struct {
OwnerID int64 OwnerID int64
Private optional.Option[bool] Private optional.Option[bool]

View File

@ -1,7 +1,7 @@
// Copyright 2021 The Gitea Authors. All rights reserved. // Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package models package repo
import ( import (
"context" "context"
@ -10,16 +10,58 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
) )
// ErrNoPendingRepoTransfer is an error type for repositories without a pending
// transfer request
type ErrNoPendingRepoTransfer struct {
RepoID int64
}
func (err ErrNoPendingRepoTransfer) Error() string {
return fmt.Sprintf("repository doesn't have a pending transfer [repo_id: %d]", err.RepoID)
}
// IsErrNoPendingTransfer is an error type when a repository has no pending
// transfers
func IsErrNoPendingTransfer(err error) bool {
_, ok := err.(ErrNoPendingRepoTransfer)
return ok
}
func (err ErrNoPendingRepoTransfer) Unwrap() error {
return util.ErrNotExist
}
// ErrRepoTransferInProgress represents the state of a repository that has an
// ongoing transfer
type ErrRepoTransferInProgress struct {
Uname string
Name string
}
// IsErrRepoTransferInProgress checks if an error is a ErrRepoTransferInProgress.
func IsErrRepoTransferInProgress(err error) bool {
_, ok := err.(ErrRepoTransferInProgress)
return ok
}
func (err ErrRepoTransferInProgress) Error() string {
return fmt.Sprintf("repository is already being transferred [uname: %s, name: %s]", err.Uname, err.Name)
}
func (err ErrRepoTransferInProgress) Unwrap() error {
return util.ErrAlreadyExist
}
// RepoTransfer is used to manage repository transfers // RepoTransfer is used to manage repository transfers
type RepoTransfer struct { type RepoTransfer struct { //nolint
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
DoerID int64 DoerID int64
Doer *user_model.User `xorm:"-"` Doer *user_model.User `xorm:"-"`
@ -126,7 +168,7 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
// process for the repository // process for the repository
func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) { func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) {
transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID}) transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID})
if err != nil { if err != nil {
return nil, err return nil, err
@ -145,11 +187,11 @@ func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error {
} }
// TestRepositoryReadyForTransfer make sure repo is ready to transfer // TestRepositoryReadyForTransfer make sure repo is ready to transfer
func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error { func TestRepositoryReadyForTransfer(status RepositoryStatus) error {
switch status { switch status {
case repo_model.RepositoryBeingMigrated: case RepositoryBeingMigrated:
return errors.New("repo is not ready, currently migrating") return errors.New("repo is not ready, currently migrating")
case repo_model.RepositoryPendingTransfer: case RepositoryPendingTransfer:
return ErrRepoTransferInProgress{} return ErrRepoTransferInProgress{}
} }
return nil return nil
@ -159,7 +201,7 @@ func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error {
// it marks the repository transfer as "pending" // it marks the repository transfer as "pending"
func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team) error { func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team) error {
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
repo, err := repo_model.GetRepositoryByID(ctx, repoID) repo, err := GetRepositoryByID(ctx, repoID)
if err != nil { if err != nil {
return err return err
} }
@ -169,16 +211,16 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
return err return err
} }
repo.Status = repo_model.RepositoryPendingTransfer repo.Status = RepositoryPendingTransfer
if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil {
return err return err
} }
// Check if new owner has repository with same name. // Check if new owner has repository with same name.
if has, err := repo_model.IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil { if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err) return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has { } else if has {
return repo_model.ErrRepoAlreadyExist{ return ErrRepoAlreadyExist{
Uname: newOwner.LowerName, Uname: newOwner.LowerName,
Name: repo.Name, Name: repo.Name,
} }

View File

@ -14,13 +14,13 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -235,5 +235,5 @@ func PrepareTestEnv(t testing.TB) {
assert.NoError(t, PrepareTestDatabase()) assert.NoError(t, PrepareTestDatabase())
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta") metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath)) assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set test.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
} }

View File

@ -788,6 +788,21 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
return committer.Commit() return committer.Commit()
} }
// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error.
type ErrDeleteLastAdminUser struct {
UID int64
}
// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser.
func IsErrDeleteLastAdminUser(err error) bool {
_, ok := err.(ErrDeleteLastAdminUser)
return ok
}
func (err ErrDeleteLastAdminUser) Error() string {
return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID)
}
// IsLastAdminUser check whether user is the last admin // IsLastAdminUser check whether user is the last admin
func IsLastAdminUser(ctx context.Context, user *User) bool { func IsLastAdminUser(ctx context.Context, user *User) bool {
if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 { if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 {

View File

@ -1,9 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package base
type (
// TplName template relative path type
TplName string
)

View File

@ -13,9 +13,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"hash" "hash"
"os"
"path/filepath"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -189,49 +186,3 @@ func EntryIcon(entry *git.TreeEntry) string {
return "file" return "file"
} }
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
func SetupGiteaRoot() string {
giteaRoot := os.Getenv("GITEA_ROOT")
if giteaRoot == "" {
_, filename, _, _ := runtime.Caller(0)
giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go")
wd, err := os.Getwd()
if err != nil {
rel, err := filepath.Rel(giteaRoot, wd)
if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") {
giteaRoot = wd
}
}
if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) {
giteaRoot = ""
} else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil {
giteaRoot = ""
}
}
return giteaRoot
}
// FormatNumberSI format a number
func FormatNumberSI(data any) string {
var num int64
if num1, ok := data.(int64); ok {
num = num1
} else if num1, ok := data.(int); ok {
num = int64(num1)
} else {
return ""
}
if num < 1000 {
return fmt.Sprintf("%d", num)
} else if num < 1000000 {
num2 := float32(num) / float32(1000.0)
return fmt.Sprintf("%.1fk", num2)
} else if num < 1000000000 {
num2 := float32(num) / float32(1000000.0)
return fmt.Sprintf("%.1fM", num2)
}
num2 := float32(num) / float32(1000000000.0)
return fmt.Sprintf("%.1fG", num2)
}

View File

@ -169,18 +169,3 @@ func TestInt64sToStrings(t *testing.T) {
} }
// TODO: Test EntryIcon // TODO: Test EntryIcon
func TestSetupGiteaRoot(t *testing.T) {
t.Setenv("GITEA_ROOT", "test")
assert.Equal(t, "test", SetupGiteaRoot())
t.Setenv("GITEA_ROOT", "")
assert.NotEqual(t, "test", SetupGiteaRoot())
}
func TestFormatNumberSI(t *testing.T) {
assert.Equal(t, "125", FormatNumberSI(int(125)))
assert.Equal(t, "1.3k", FormatNumberSI(int64(1317)))
assert.Equal(t, "21.3M", FormatNumberSI(21317675))
assert.Equal(t, "45.7G", FormatNumberSI(45721317675))
assert.Equal(t, "", FormatNumberSI("test"))
}

View File

@ -7,10 +7,8 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"math" "math"
"runtime"
"strconv" "strconv"
"strings" "strings"
@ -32,7 +30,6 @@ type WriteCloserError interface {
func ensureValidGitRepository(ctx context.Context, repoPath string) error { func ensureValidGitRepository(ctx context.Context, repoPath string) error {
stderr := strings.Builder{} stderr := strings.Builder{}
err := NewCommand(ctx, "rev-parse"). err := NewCommand(ctx, "rev-parse").
SetDescription(fmt.Sprintf("%s rev-parse [repo_path: %s]", GitExecutable, repoPath)).
Run(&RunOpts{ Run(&RunOpts{
Dir: repoPath, Dir: repoPath,
Stderr: &stderr, Stderr: &stderr,
@ -62,13 +59,9 @@ func catFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError,
cancel() cancel()
}() }()
_, filename, line, _ := runtime.Caller(2)
filename = strings.TrimPrefix(filename, callerPrefix)
go func() { go func() {
stderr := strings.Builder{} stderr := strings.Builder{}
err := NewCommand(ctx, "cat-file", "--batch-check"). err := NewCommand(ctx, "cat-file", "--batch-check").
SetDescription(fmt.Sprintf("%s cat-file --batch-check [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)).
Run(&RunOpts{ Run(&RunOpts{
Dir: repoPath, Dir: repoPath,
Stdin: batchStdinReader, Stdin: batchStdinReader,
@ -114,13 +107,9 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
cancel() cancel()
}() }()
_, filename, line, _ := runtime.Caller(2)
filename = strings.TrimPrefix(filename, callerPrefix)
go func() { go func() {
stderr := strings.Builder{} stderr := strings.Builder{}
err := NewCommand(ctx, "cat-file", "--batch"). err := NewCommand(ctx, "cat-file", "--batch").
SetDescription(fmt.Sprintf("%s cat-file --batch [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)).
Run(&RunOpts{ Run(&RunOpts{
Dir: repoPath, Dir: repoPath,
Stdin: batchStdinReader, Stdin: batchStdinReader,
@ -320,13 +309,6 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu
return mode, fname, sha, n, err return mode, fname, sha, n, err
} }
var callerPrefix string
func init() {
_, filename, _, _ := runtime.Caller(0)
callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go")
}
func DiscardFull(rd *bufio.Reader, discard int64) error { func DiscardFull(rd *bufio.Reader, discard int64) error {
if discard > math.MaxInt32 { if discard > math.MaxInt32 {
n, err := rd.Discard(math.MaxInt32) n, err := rd.Discard(math.MaxInt32)

View File

@ -7,7 +7,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"os" "os"
@ -142,9 +141,7 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend. // There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile) cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile)
} }
cmd.AddDynamicArguments(commit.ID.String()). cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
AddDashesAndList(file).
SetDescription(fmt.Sprintf("GetBlame [repo_path: %s]", repoPath))
reader, stdout, err := os.Pipe() reader, stdout, err := os.Pipe()
if err != nil { if err != nil {
if ignoreRevsFile != nil { if ignoreRevsFile != nil {

View File

@ -12,6 +12,7 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@ -43,18 +44,24 @@ type Command struct {
prog string prog string
args []string args []string
parentContext context.Context parentContext context.Context
desc string
globalArgsLength int globalArgsLength int
brokenArgs []string brokenArgs []string
} }
func (c *Command) String() string { func logArgSanitize(arg string) string {
return c.toString(false) if strings.Contains(arg, "://") && strings.Contains(arg, "@") {
return util.SanitizeCredentialURLs(arg)
} else if filepath.IsAbs(arg) {
base := filepath.Base(arg)
dir := filepath.Dir(arg)
return filepath.Join(filepath.Base(dir), base)
}
return arg
} }
func (c *Command) toString(sanitizing bool) string { func (c *Command) LogString() string {
// WARNING: this function is for debugging purposes only. It's much better than old code (which only joins args with space), // WARNING: this function is for debugging purposes only. It's much better than old code (which only joins args with space),
// It's impossible to make a simple and 100% correct implementation of argument quoting for different platforms. // It's impossible to make a simple and 100% correct implementation of argument quoting for different platforms here.
debugQuote := func(s string) string { debugQuote := func(s string) string {
if strings.ContainsAny(s, " `'\"\t\r\n") { if strings.ContainsAny(s, " `'\"\t\r\n") {
return fmt.Sprintf("%q", s) return fmt.Sprintf("%q", s)
@ -63,12 +70,11 @@ func (c *Command) toString(sanitizing bool) string {
} }
a := make([]string, 0, len(c.args)+1) a := make([]string, 0, len(c.args)+1)
a = append(a, debugQuote(c.prog)) a = append(a, debugQuote(c.prog))
for _, arg := range c.args { if c.globalArgsLength > 0 {
if sanitizing && (strings.Contains(arg, "://") && strings.Contains(arg, "@")) { a = append(a, "...global...")
a = append(a, debugQuote(util.SanitizeCredentialURLs(arg)))
} else {
a = append(a, debugQuote(arg))
} }
for i := c.globalArgsLength; i < len(c.args); i++ {
a = append(a, debugQuote(logArgSanitize(c.args[i])))
} }
return strings.Join(a, " ") return strings.Join(a, " ")
} }
@ -112,12 +118,6 @@ func (c *Command) SetParentContext(ctx context.Context) *Command {
return c return c
} }
// SetDescription sets the description for this command which be returned on c.String()
func (c *Command) SetDescription(desc string) *Command {
c.desc = desc
return c
}
// isSafeArgumentValue checks if the argument is safe to be used as a value (not an option) // isSafeArgumentValue checks if the argument is safe to be used as a value (not an option)
func isSafeArgumentValue(s string) bool { func isSafeArgumentValue(s string) bool {
return s == "" || s[0] != '-' return s == "" || s[0] != '-'
@ -271,8 +271,12 @@ var ErrBrokenCommand = errors.New("git command is broken")
// Run runs the command with the RunOpts // Run runs the command with the RunOpts
func (c *Command) Run(opts *RunOpts) error { func (c *Command) Run(opts *RunOpts) error {
return c.run(1, opts)
}
func (c *Command) run(skip int, opts *RunOpts) error {
if len(c.brokenArgs) != 0 { if len(c.brokenArgs) != 0 {
log.Error("git command is broken: %s, broken args: %s", c.String(), strings.Join(c.brokenArgs, " ")) log.Error("git command is broken: %s, broken args: %s", c.LogString(), strings.Join(c.brokenArgs, " "))
return ErrBrokenCommand return ErrBrokenCommand
} }
if opts == nil { if opts == nil {
@ -285,20 +289,14 @@ func (c *Command) Run(opts *RunOpts) error {
timeout = defaultCommandExecutionTimeout timeout = defaultCommandExecutionTimeout
} }
if len(opts.Dir) == 0 { var desc string
log.Debug("git.Command.Run: %s", c) callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */)
} else { if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
log.Debug("git.Command.RunDir(%s): %s", opts.Dir, c) callerInfo = callerInfo[pos+1:]
}
desc := c.desc
if desc == "" {
if opts.Dir == "" {
desc = fmt.Sprintf("git: %s", c.toString(true))
} else {
desc = fmt.Sprintf("git(dir:%s): %s", opts.Dir, c.toString(true))
}
} }
// these logs are for debugging purposes only, so no guarantee of correctness or stability
desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString())
log.Debug("git.Command: %s", desc)
var ctx context.Context var ctx context.Context
var cancel context.CancelFunc var cancel context.CancelFunc
@ -401,7 +399,7 @@ func IsErrorExitCode(err error, code int) bool {
// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). // RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) { func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) {
stdoutBytes, stderrBytes, err := c.RunStdBytes(opts) stdoutBytes, stderrBytes, err := c.runStdBytes(opts)
stdout = util.UnsafeBytesToString(stdoutBytes) stdout = util.UnsafeBytesToString(stdoutBytes)
stderr = util.UnsafeBytesToString(stderrBytes) stderr = util.UnsafeBytesToString(stderrBytes)
if err != nil { if err != nil {
@ -413,6 +411,10 @@ func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr Run
// RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). // RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr).
func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) {
return c.runStdBytes(opts)
}
func (c *Command) runStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) {
if opts == nil { if opts == nil {
opts = &RunOpts{} opts = &RunOpts{}
} }
@ -435,7 +437,7 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
PipelineFunc: opts.PipelineFunc, PipelineFunc: opts.PipelineFunc,
} }
err := c.Run(newOpts) err := c.run(2, newOpts)
stderr = stderrBuf.Bytes() stderr = stderrBuf.Bytes()
if err != nil { if err != nil {
return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)} return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)}

View File

@ -55,8 +55,8 @@ func TestGitArgument(t *testing.T) {
func TestCommandString(t *testing.T) { func TestCommandString(t *testing.T) {
cmd := NewCommandContextNoGlobals(context.Background(), "a", "-m msg", "it's a test", `say "hello"`) cmd := NewCommandContextNoGlobals(context.Background(), "a", "-m msg", "it's a test", `say "hello"`)
assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.String()) assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString())
cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/") cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b")
assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/"`, cmd.toString(true)) assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString())
} }

View File

@ -5,8 +5,12 @@ package git
import ( import (
"context" "context"
"fmt"
"net/url"
"strings"
giturl "code.gitea.io/gitea/modules/git/url" giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/util"
) )
// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name // GetRemoteAddress returns remote url of git repository in the repoPath with special remote name
@ -37,3 +41,61 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git
} }
return giturl.Parse(addr) return giturl.Parse(addr)
} }
// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error.
type ErrInvalidCloneAddr struct {
Host string
IsURLError bool
IsInvalidPath bool
IsProtocolInvalid bool
IsPermissionDenied bool
LocalPath bool
}
// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr.
func IsErrInvalidCloneAddr(err error) bool {
_, ok := err.(*ErrInvalidCloneAddr)
return ok
}
func (err *ErrInvalidCloneAddr) Error() string {
if err.IsInvalidPath {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided path is invalid", err.Host)
}
if err.IsProtocolInvalid {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url protocol is not allowed", err.Host)
}
if err.IsPermissionDenied {
return fmt.Sprintf("migration/cloning from '%s' is not allowed.", err.Host)
}
if err.IsURLError {
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url is invalid", err.Host)
}
return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host)
}
func (err *ErrInvalidCloneAddr) Unwrap() error {
return util.ErrInvalidArgument
}
// ParseRemoteAddr checks if given remote address is valid,
// and returns composed URL with needed username and password.
func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, error) {
remoteAddr = strings.TrimSpace(remoteAddr)
// Remote address can be HTTP/HTTPS/Git URL or local path.
if strings.HasPrefix(remoteAddr, "http://") ||
strings.HasPrefix(remoteAddr, "https://") ||
strings.HasPrefix(remoteAddr, "git://") {
u, err := url.Parse(remoteAddr)
if err != nil {
return "", &ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr}
}
if len(authUsername)+len(authPassword) > 0 {
u.User = url.UserPassword(authUsername, authPassword)
}
remoteAddr = u.String()
}
return remoteAddr, nil
}

View File

@ -18,7 +18,6 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/util"
) )
// GPGSettings represents the default GPG settings for this repository // GPGSettings represents the default GPG settings for this repository
@ -160,12 +159,6 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op
} }
cmd.AddDashesAndList(from, to) cmd.AddDashesAndList(from, to)
if strings.Contains(from, "://") && strings.Contains(from, "@") {
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, util.SanitizeCredentialURLs(from), to, opts.Shared, opts.Mirror, opts.Depth))
} else {
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, from, to, opts.Shared, opts.Mirror, opts.Depth))
}
if opts.Timeout <= 0 { if opts.Timeout <= 0 {
opts.Timeout = -1 opts.Timeout = -1
} }
@ -213,12 +206,6 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error {
} }
cmd.AddDashesAndList(remoteBranchArgs...) cmd.AddDashesAndList(remoteBranchArgs...)
if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {
cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror))
} else {
cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror))
}
stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath}) stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath})
if err != nil { if err != nil {
if strings.Contains(stderr, "non-fast-forward") { if strings.Contains(stderr, "non-fast-forward") {

View File

@ -9,6 +9,7 @@ import (
"sync" "sync"
"time" "time"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -136,7 +137,7 @@ func (g *Manager) doShutdown() {
} }
g.lock.Lock() g.lock.Lock()
g.shutdownCtxCancel() g.shutdownCtxCancel()
atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "post-shutdown")) atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-shutdown"))
pprof.SetGoroutineLabels(atShutdownCtx) pprof.SetGoroutineLabels(atShutdownCtx)
for _, fn := range g.toRunAtShutdown { for _, fn := range g.toRunAtShutdown {
go fn() go fn()
@ -167,7 +168,7 @@ func (g *Manager) doHammerTime(d time.Duration) {
default: default:
log.Warn("Setting Hammer condition") log.Warn("Setting Hammer condition")
g.hammerCtxCancel() g.hammerCtxCancel()
atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "post-hammer")) atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-hammer"))
pprof.SetGoroutineLabels(atHammerCtx) pprof.SetGoroutineLabels(atHammerCtx)
} }
g.lock.Unlock() g.lock.Unlock()
@ -183,7 +184,7 @@ func (g *Manager) doTerminate() {
default: default:
log.Warn("Terminating") log.Warn("Terminating")
g.terminateCtxCancel() g.terminateCtxCancel()
atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "post-terminate")) atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-terminate"))
pprof.SetGoroutineLabels(atTerminateCtx) pprof.SetGoroutineLabels(atTerminateCtx)
for _, fn := range g.toRunAtTerminate { for _, fn := range g.toRunAtTerminate {

View File

@ -8,6 +8,8 @@ import (
"runtime/pprof" "runtime/pprof"
"sync" "sync"
"time" "time"
"code.gitea.io/gitea/modules/gtprof"
) )
// FIXME: it seems that there is a bug when using systemd Type=notify: the "Install Page" (INSTALL_LOCK=false) doesn't notify properly. // FIXME: it seems that there is a bug when using systemd Type=notify: the "Install Page" (INSTALL_LOCK=false) doesn't notify properly.
@ -65,10 +67,10 @@ func (g *Manager) prepare(ctx context.Context) {
g.hammerCtx, g.hammerCtxCancel = context.WithCancel(ctx) g.hammerCtx, g.hammerCtxCancel = context.WithCancel(ctx)
g.managerCtx, g.managerCtxCancel = context.WithCancel(ctx) g.managerCtx, g.managerCtxCancel = context.WithCancel(ctx)
g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "with-terminate")) g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-terminate"))
g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels("graceful-lifecycle", "with-shutdown")) g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-shutdown"))
g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "with-hammer")) g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-hammer"))
g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "with-manager")) g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-manager"))
if !g.setStateTransition(stateInit, stateRunning) { if !g.setStateTransition(stateInit, stateRunning) {
panic("invalid graceful manager state: transition from init to running failed") panic("invalid graceful manager state: transition from init to running failed")

25
modules/gtprof/gtprof.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gtprof
// This is a Gitea-specific profiling package,
// the name is chosen to distinguish it from the standard pprof tool and "GNU gprof"
// LabelGracefulLifecycle is a label marking manager lifecycle phase
// Making it compliant with prometheus key regex https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
// would enable someone interested to be able to continuously gather profiles into pyroscope.
// Other labels for pprof should also follow this rule.
const LabelGracefulLifecycle = "graceful_lifecycle"
// LabelPid is a label set on goroutines that have a process attached
const LabelPid = "pid"
// LabelPpid is a label set on goroutines that have a process attached
const LabelPpid = "ppid"
// LabelProcessType is a label set on goroutines that have a process attached
const LabelProcessType = "process_type"
// LabelProcessDescription is a label set on goroutines that have a process attached
const LabelProcessDescription = "process_description"

View File

@ -13,7 +13,6 @@ import (
type Event struct { type Event struct {
Time time.Time Time time.Time
GoroutinePid string
Caller string Caller string
Filename string Filename string
Line int Line int
@ -218,18 +217,17 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms
} }
if flags&Lgopid == Lgopid { if flags&Lgopid == Lgopid {
if event.GoroutinePid != "" { deprecatedGoroutinePid := "no-gopid" // use a dummy value to avoid breaking the log format
buf = append(buf, '[') buf = append(buf, '[')
if mode.Colorize { if mode.Colorize {
buf = append(buf, ColorBytes(FgHiYellow)...) buf = append(buf, ColorBytes(FgHiYellow)...)
} }
buf = append(buf, event.GoroutinePid...) buf = append(buf, deprecatedGoroutinePid...)
if mode.Colorize { if mode.Colorize {
buf = append(buf, resetBytes...) buf = append(buf, resetBytes...)
} }
buf = append(buf, ']', ' ') buf = append(buf, ']', ' ')
} }
}
buf = append(buf, msg...) buf = append(buf, msg...)
if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level { if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level {

View File

@ -28,14 +28,13 @@ func TestEventFormatTextMessage(t *testing.T) {
Caller: "caller", Caller: "caller",
Filename: "filename", Filename: "filename",
Line: 123, Line: 123,
GoroutinePid: "pid",
Level: ERROR, Level: ERROR,
Stacktrace: "stacktrace", Stacktrace: "stacktrace",
}, },
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue), "msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
) )
assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [pid] msg format: arg0 arg1 assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [no-gopid] msg format: arg0 arg1
stacktrace stacktrace
`, string(res)) `, string(res))
@ -46,12 +45,11 @@ func TestEventFormatTextMessage(t *testing.T) {
Caller: "caller", Caller: "caller",
Filename: "filename", Filename: "filename",
Line: 123, Line: 123,
GoroutinePid: "pid",
Level: ERROR, Level: ERROR,
Stacktrace: "stacktrace", Stacktrace: "stacktrace",
}, },
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue), "msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
) )
assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mpid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res)) assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mno-gopid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res))
} }

View File

@ -30,7 +30,7 @@ const (
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
Llevelinitial // Initial character of the provided level in brackets, eg. [I] for info Llevelinitial // Initial character of the provided level in brackets, eg. [I] for info
Llevel // Provided level in brackets [INFO] Llevel // Provided level in brackets [INFO]
Lgopid // the Goroutine-PID of the context Lgopid // the Goroutine-PID of the context, deprecated and it is always a const value
Lmedfile = Lshortfile | Llongfile // last 20 characters of the filename Lmedfile = Lshortfile | Llongfile // last 20 characters of the filename
LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default

View File

@ -1,19 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import "unsafe"
//go:linkname runtime_getProfLabel runtime/pprof.runtime_getProfLabel
func runtime_getProfLabel() unsafe.Pointer //nolint
type labelMap map[string]string
func getGoroutineLabels() map[string]string {
l := (*labelMap)(runtime_getProfLabel())
if l == nil {
return nil
}
return *l
}

View File

@ -1,33 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package log
import (
"context"
"runtime/pprof"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getGoroutineLabels(t *testing.T) {
pprof.Do(context.Background(), pprof.Labels(), func(ctx context.Context) {
currentLabels := getGoroutineLabels()
pprof.ForLabels(ctx, func(key, value string) bool {
assert.EqualValues(t, value, currentLabels[key])
return true
})
pprof.Do(ctx, pprof.Labels("Test_getGoroutineLabels", "Test_getGoroutineLabels_child1"), func(ctx context.Context) {
currentLabels := getGoroutineLabels()
pprof.ForLabels(ctx, func(key, value string) bool {
assert.EqualValues(t, value, currentLabels[key])
return true
})
if assert.NotNil(t, currentLabels) {
assert.EqualValues(t, "Test_getGoroutineLabels_child1", currentLabels["Test_getGoroutineLabels"])
}
})
})
}

View File

@ -200,11 +200,6 @@ func (l *LoggerImpl) Log(skip int, level Level, format string, logArgs ...any) {
event.Stacktrace = Stack(skip + 1) event.Stacktrace = Stack(skip + 1)
} }
labels := getGoroutineLabels()
if labels != nil {
event.GoroutinePid = labels["pid"]
}
// get a simple text message without color // get a simple text message without color
msgArgs := make([]any, len(logArgs)) msgArgs := make([]any, len(logArgs))
copy(msgArgs, logArgs) copy(msgArgs, logArgs)

View File

@ -24,7 +24,7 @@ type GlobalVarsType struct {
LinkRegex *regexp.Regexp // fast matching a URL link, no any extra validation. LinkRegex *regexp.Regexp // fast matching a URL link, no any extra validation.
} }
var GlobalVars = sync.OnceValue[*GlobalVarsType](func() *GlobalVarsType { var GlobalVars = sync.OnceValue(func() *GlobalVarsType {
v := &GlobalVarsType{} v := &GlobalVarsType{}
v.wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`) v.wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://") v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://")

View File

@ -42,7 +42,7 @@ type globalVarsType struct {
nulCleaner *strings.Replacer nulCleaner *strings.Replacer
} }
var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType { var globalVars = sync.OnceValue(func() *globalVarsType {
v := &globalVarsType{} v := &globalVarsType{}
// NOTE: All below regex matching do not perform any extra validation. // NOTE: All below regex matching do not perform any extra validation.
// Thus a link is produced even if the linked entity does not exist. // Thus a link is produced even if the linked entity does not exist.

View File

@ -17,7 +17,7 @@ import (
"golang.org/x/net/html" "golang.org/x/net/html"
) )
var reAttrClass = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp { var reAttrClass = sync.OnceValue(func() *regexp.Regexp {
// TODO: it isn't a problem at the moment because our HTML contents are always well constructed // TODO: it isn't a problem at the moment because our HTML contents are always well constructed
return regexp.MustCompile(`(<[^>]+)\s+class="([^"]+)"([^>]*>)`) return regexp.MustCompile(`(<[^>]+)\s+class="([^"]+)"([^>]*>)`)
}) })

View File

@ -112,7 +112,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
} }
// it is copied from old code, which is quite doubtful whether it is correct // it is copied from old code, which is quite doubtful whether it is correct
var reValidIconName = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp { var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$") return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
}) })

View File

@ -129,7 +129,8 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
Enabled: setting.Markdown.EnableMath, Enabled: setting.Markdown.EnableMath,
ParseDollarInline: true, ParseDollarInline: true,
ParseDollarBlock: true, ParseDollarBlock: true,
ParseSquareBlock: true, // TODO: this is a bad syntax, it should be deprecated in the future (by some config options) ParseSquareBlock: true, // TODO: this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping, it should be deprecated in the future (by some config options)
// ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future
}), }),
meta.Meta, meta.Meta,
), ),

View File

@ -44,7 +44,8 @@ var (
// https://man.archlinux.org/man/PKGBUILD.5 // https://man.archlinux.org/man/PKGBUILD.5
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`) namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) // (epoch:pkgver-pkgrel)
versionPattern = regexp.MustCompile(`\A(?:\d:)?[\w.+~]+(?:-[-\w.+~]+)?\z`)
) )
type Package struct { type Package struct {
@ -69,10 +70,12 @@ type FileMetadata struct {
Packager string `json:"packager,omitempty"` Packager string `json:"packager,omitempty"`
Groups []string `json:"groups,omitempty"` Groups []string `json:"groups,omitempty"`
Provides []string `json:"provides,omitempty"` Provides []string `json:"provides,omitempty"`
Replaces []string `json:"replaces,omitempty"`
Depends []string `json:"depends,omitempty"` Depends []string `json:"depends,omitempty"`
OptDepends []string `json:"opt_depends,omitempty"` OptDepends []string `json:"opt_depends,omitempty"`
MakeDepends []string `json:"make_depends,omitempty"` MakeDepends []string `json:"make_depends,omitempty"`
CheckDepends []string `json:"check_depends,omitempty"` CheckDepends []string `json:"check_depends,omitempty"`
Conflicts []string `json:"conflicts,omitempty"`
XData []string `json:"xdata,omitempty"` XData []string `json:"xdata,omitempty"`
Backup []string `json:"backup,omitempty"` Backup []string `json:"backup,omitempty"`
Files []string `json:"files,omitempty"` Files []string `json:"files,omitempty"`
@ -201,12 +204,16 @@ func ParsePackageInfo(r io.Reader) (*Package, error) {
p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
case "depend": case "depend":
p.FileMetadata.Depends = append(p.FileMetadata.Depends, value) p.FileMetadata.Depends = append(p.FileMetadata.Depends, value)
case "replaces":
p.FileMetadata.Replaces = append(p.FileMetadata.Replaces, value)
case "optdepend": case "optdepend":
p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value) p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value)
case "makedepend": case "makedepend":
p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value) p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value)
case "checkdepend": case "checkdepend":
p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value) p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value)
case "conflict":
p.FileMetadata.Conflicts = append(p.FileMetadata.Conflicts, value)
case "backup": case "backup":
p.FileMetadata.Backup = append(p.FileMetadata.Backup, value) p.FileMetadata.Backup = append(p.FileMetadata.Backup, value)
case "group": case "group":

View File

@ -42,8 +42,10 @@ depend = gitea
provides = common provides = common
provides = gitea provides = gitea
optdepend = hex optdepend = hex
replaces = gogs
checkdepend = common checkdepend = common
makedepend = cmake makedepend = cmake
conflict = ninja
backup = usr/bin/paket1`) backup = usr/bin/paket1`)
} }
@ -120,6 +122,14 @@ func TestParsePackageInfo(t *testing.T) {
assert.ErrorIs(t, err, ErrInvalidName) assert.ErrorIs(t, err, ErrInvalidName)
}) })
t.Run("Regexp", func(t *testing.T) {
assert.Regexp(t, versionPattern, "1.2_3~4+5")
assert.Regexp(t, versionPattern, "1:2_3~4+5")
assert.NotRegexp(t, versionPattern, "a:1.0.0-1")
assert.NotRegexp(t, versionPattern, "0.0.1/1-1")
assert.NotRegexp(t, versionPattern, "1.0.0 -1")
})
t.Run("InvalidVersion", func(t *testing.T) { t.Run("InvalidVersion", func(t *testing.T) {
data := createPKGINFOContent(packageName, "") data := createPKGINFOContent(packageName, "")
@ -149,8 +159,10 @@ func TestParsePackageInfo(t *testing.T) {
assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups) assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends) assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends)
assert.ElementsMatch(t, []string{"gogs"}, p.FileMetadata.Replaces)
assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends) assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends)
assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends) assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends)
assert.ElementsMatch(t, []string{"ninja"}, p.FileMetadata.Conflicts)
assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends) assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends)
assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup) assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup)
}) })

View File

@ -32,7 +32,7 @@ func (c *Context) Value(key any) any {
} }
// ProcessContextKey is the key under which process contexts are stored // ProcessContextKey is the key under which process contexts are stored
var ProcessContextKey any = "process-context" var ProcessContextKey any = "process_context"
// GetContext will return a process context if one exists // GetContext will return a process context if one exists
func GetContext(ctx context.Context) *Context { func GetContext(ctx context.Context) *Context {

View File

@ -11,6 +11,8 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"code.gitea.io/gitea/modules/gtprof"
) )
// TODO: This packages still uses a singleton for the Manager. // TODO: This packages still uses a singleton for the Manager.
@ -25,18 +27,6 @@ var (
DefaultContext = context.Background() DefaultContext = context.Background()
) )
// DescriptionPProfLabel is a label set on goroutines that have a process attached
const DescriptionPProfLabel = "process-description"
// PIDPProfLabel is a label set on goroutines that have a process attached
const PIDPProfLabel = "pid"
// PPIDPProfLabel is a label set on goroutines that have a process attached
const PPIDPProfLabel = "ppid"
// ProcessTypePProfLabel is a label set on goroutines that have a process attached
const ProcessTypePProfLabel = "process-type"
// IDType is a pid type // IDType is a pid type
type IDType string type IDType string
@ -187,7 +177,12 @@ func (pm *Manager) Add(ctx context.Context, description string, cancel context.C
Trace(true, pid, description, parentPID, processType) Trace(true, pid, description, parentPID, processType)
pprofCtx := pprof.WithLabels(ctx, pprof.Labels(DescriptionPProfLabel, description, PPIDPProfLabel, string(parentPID), PIDPProfLabel, string(pid), ProcessTypePProfLabel, processType)) pprofCtx := pprof.WithLabels(ctx, pprof.Labels(
gtprof.LabelProcessDescription, description,
gtprof.LabelPpid, string(parentPID),
gtprof.LabelPid, string(pid),
gtprof.LabelProcessType, processType,
))
if currentlyRunning { if currentlyRunning {
pprof.SetGoroutineLabels(pprofCtx) pprof.SetGoroutineLabels(pprofCtx)
} }

View File

@ -10,6 +10,8 @@ import (
"sort" "sort"
"time" "time"
"code.gitea.io/gitea/modules/gtprof"
"github.com/google/pprof/profile" "github.com/google/pprof/profile"
) )
@ -202,7 +204,7 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int
// Add the non-process associated labels from the goroutine sample to the Stack // Add the non-process associated labels from the goroutine sample to the Stack
for name, value := range sample.Label { for name, value := range sample.Label {
if name == DescriptionPProfLabel || name == PIDPProfLabel || (!flat && name == PPIDPProfLabel) || name == ProcessTypePProfLabel { if name == gtprof.LabelProcessDescription || name == gtprof.LabelPid || (!flat && name == gtprof.LabelPpid) || name == gtprof.LabelProcessType {
continue continue
} }
@ -224,7 +226,7 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int
var process *Process var process *Process
// Try to get the PID from the goroutine labels // Try to get the PID from the goroutine labels
if pidvalue, ok := sample.Label[PIDPProfLabel]; ok && len(pidvalue) == 1 { if pidvalue, ok := sample.Label[gtprof.LabelPid]; ok && len(pidvalue) == 1 {
pid := IDType(pidvalue[0]) pid := IDType(pidvalue[0])
// Now try to get the process from our map // Now try to get the process from our map
@ -238,20 +240,20 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int
// get the parent PID // get the parent PID
ppid := IDType("") ppid := IDType("")
if value, ok := sample.Label[PPIDPProfLabel]; ok && len(value) == 1 { if value, ok := sample.Label[gtprof.LabelPpid]; ok && len(value) == 1 {
ppid = IDType(value[0]) ppid = IDType(value[0])
} }
// format the description // format the description
description := "(dead process)" description := "(dead process)"
if value, ok := sample.Label[DescriptionPProfLabel]; ok && len(value) == 1 { if value, ok := sample.Label[gtprof.LabelProcessDescription]; ok && len(value) == 1 {
description = value[0] + " " + description description = value[0] + " " + description
} }
// override the type of the process to "code" but add the old type as a label on the first stack // override the type of the process to "code" but add the old type as a label on the first stack
ptype := NoneProcessType ptype := NoneProcessType
if value, ok := sample.Label[ProcessTypePProfLabel]; ok && len(value) == 1 { if value, ok := sample.Label[gtprof.LabelProcessType]; ok && len(value) == 1 {
stack.Labels = append(stack.Labels, &Label{Name: ProcessTypePProfLabel, Value: value[0]}) stack.Labels = append(stack.Labels, &Label{Name: gtprof.LabelProcessType, Value: value[0]})
} }
process = &Process{ process = &Process{
PID: pid, PID: pid,

View File

@ -6,6 +6,7 @@ package queue
import ( import (
"context" "context"
"fmt" "fmt"
"runtime/pprof"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -241,6 +242,9 @@ func NewWorkerPoolQueueWithContext[T any](ctx context.Context, name string, queu
w.origHandler = handler w.origHandler = handler
w.safeHandler = func(t ...T) (unhandled []T) { w.safeHandler = func(t ...T) (unhandled []T) {
defer func() { defer func() {
// FIXME: there is no ctx support in the handler, so process manager is unable to restore the labels
// so here we explicitly set the "queue ctx" labels again after the handler is done
pprof.SetGoroutineLabels(w.ctxRun)
err := recover() err := recover()
if err != nil { if err != nil {
log.Error("Recovered from panic in queue %q handler: %v\n%s", name, err, log.Stack(2)) log.Error("Recovered from panic in queue %q handler: %v\n%s", name, err, log.Stack(2))

View File

@ -32,7 +32,7 @@ var (
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287 // 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 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 // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. org/repo#12345 // e.g. org/repo#12345
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)

View File

@ -463,6 +463,7 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
"ABC-123:", "ABC-123:",
"\"ABC-123\"", "\"ABC-123\"",
"'ABC-123'", "'ABC-123'",
"ABC-123, unknown PR",
} }
falseTestCases := []string{ falseTestCases := []string{
"RC-08", "RC-08",

View File

@ -12,6 +12,9 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
// CanUserForkBetweenOwners returns true if user can fork between owners.
// By default, a user can fork a repository from another owner, but not from themselves.
// Many users really like to fork their own repositories, so add an experimental setting to allow this.
func CanUserForkBetweenOwners(id1, id2 int64) bool { func CanUserForkBetweenOwners(id1, id2 int64) bool {
if id1 != id2 { if id1 != id2 {
return true return true

View File

@ -31,12 +31,7 @@ func Test_getLicense(t *testing.T) {
Copyright (c) 2023 Gitea Copyright (c) 2023 Gitea
Permission 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: Permission is hereby granted`,
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE 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.
`,
wantErr: assert.NoError, wantErr: assert.NoError,
}, },
{ {
@ -53,7 +48,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) { if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) {
return return
} }
assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values) assert.Contains(t, string(got), tt.want, "GetLicense(%v, %v)", tt.args.name, tt.args.values)
}) })
} }
} }

View File

@ -11,6 +11,7 @@ var defaultI18nLangNames = []string{
"zh-TW", "繁體中文(台灣)", "zh-TW", "繁體中文(台灣)",
"de-DE", "Deutsch", "de-DE", "Deutsch",
"fr-FR", "Français", "fr-FR", "Français",
"ga-IE", "Gaeilge",
"nl-NL", "Nederlands", "nl-NL", "Nederlands",
"lv-LV", "Latviešu", "lv-LV", "Latviešu",
"ru-RU", "Русский", "ru-RU", "Русский",

View File

@ -9,7 +9,6 @@ import (
"html" "html"
"html/template" "html/template"
"net/url" "net/url"
"reflect"
"strings" "strings"
"time" "time"
@ -69,7 +68,7 @@ func NewFuncMap() template.FuncMap {
// ----------------------------------------------------------------- // -----------------------------------------------------------------
// time / number / format // time / number / format
"FileSize": base.FileSize, "FileSize": base.FileSize,
"CountFmt": base.FormatNumberSI, "CountFmt": countFmt,
"Sec2Time": util.SecToTime, "Sec2Time": util.SecToTime,
"TimeEstimateString": timeEstimateString, "TimeEstimateString": timeEstimateString,
@ -239,29 +238,8 @@ func iif(condition any, vals ...any) any {
} }
func isTemplateTruthy(v any) bool { func isTemplateTruthy(v any) bool {
if v == nil { truth, _ := template.IsTrue(v)
return false return truth
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Bool:
return rv.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() != 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rv.Uint() != 0
case reflect.Float32, reflect.Float64:
return rv.Float() != 0
case reflect.Complex64, reflect.Complex128:
return rv.Complex() != 0
case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
return rv.Len() > 0
case reflect.Struct:
return true
default:
return !rv.IsNil()
}
} }
// evalTokens evaluates the expression by tokens and returns the result, see the comment of eval.Expr for details. // evalTokens evaluates the expression by tokens and returns the result, see the comment of eval.Expr for details.
@ -286,14 +264,6 @@ func userThemeName(user *user_model.User) string {
return setting.UI.DefaultTheme return setting.UI.DefaultTheme
} }
func timeEstimateString(timeSec any) string {
v, _ := util.ToInt64(timeSec)
if v == 0 {
return ""
}
return util.TimeEstimateString(v)
}
// QueryBuild builds a query string from a list of key-value pairs. // QueryBuild builds a query string from a list of key-value pairs.
// It omits the nil and empty strings, but it doesn't omit other zero values, // It omits the nil and empty strings, but it doesn't omit other zero values,
// because the zero value of number types may have a meaning. // because the zero value of number types may have a meaning.

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -65,31 +66,12 @@ 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>`)) 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>`))
} }
func TestTemplateTruthy(t *testing.T) { func TestTemplateIif(t *testing.T) {
tmpl := template.New("test") tmpl := template.New("test")
tmpl.Funcs(template.FuncMap{"Iif": iif}) tmpl.Funcs(template.FuncMap{"Iif": iif})
template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`)) template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`))
cases := []any{ cases := []any{nil, false, true, "", "string", 0, 1}
nil, false, true, "", "string", 0, 1,
byte(0), byte(1), int64(0), int64(1), float64(0), float64(1),
complex(0, 0), complex(1, 0),
(chan int)(nil), make(chan int),
(func())(nil), func() {},
util.ToPointer(0), util.ToPointer(util.ToPointer(0)),
util.ToPointer(1), util.ToPointer(util.ToPointer(1)),
[0]int{},
[1]int{0},
[]int(nil),
[]int{},
[]int{0},
map[any]any(nil),
map[any]any{},
map[any]any{"k": "v"},
(*struct{})(nil),
struct{}{},
util.ToPointer(struct{}{}),
}
w := &strings.Builder{} w := &strings.Builder{}
truthyCount := 0 truthyCount := 0
for i, v := range cases { for i, v := range cases {
@ -102,3 +84,37 @@ func TestTemplateTruthy(t *testing.T) {
} }
assert.True(t, truthyCount != 0 && truthyCount != len(cases)) assert.True(t, truthyCount != 0 && truthyCount != len(cases))
} }
func TestTemplateEscape(t *testing.T) {
execTmpl := func(code string) string {
tmpl := template.New("test")
tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlutil.HTMLFormat})
template.Must(tmpl.Parse(code))
w := &strings.Builder{}
assert.NoError(t, tmpl.Execute(w, nil))
return w.String()
}
t.Run("Golang URL Escape", func(t *testing.T) {
// Golang template considers "href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping
actual := execTmpl(`<a href="?a={{"%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
actual = execTmpl(`<a data-xxx-url="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-xxx-url="?a=%25"></a>`, actual)
})
t.Run("Golang URL No-escape", func(t *testing.T) {
// non-URL content isn't auto-escaped
actual := execTmpl(`<a data-link="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-link="?a=%"></a>`, actual)
})
t.Run("QueryBuild", func(t *testing.T) {
actual := execTmpl(`<a href="{{QueryBuild "?" "a" "%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
actual = execTmpl(`<a href="?{{QueryBuild "a" "%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
})
t.Run("HTMLFormat", func(t *testing.T) {
actual := execTmpl("{{HTMLFormat `<a k=\"%s\">%s</a>` `\"` `<>`}}")
assert.Equal(t, `<a k="&#34;">&lt;&gt;</a>`, actual)
})
}

View File

@ -29,6 +29,8 @@ import (
type TemplateExecutor scopedtmpl.TemplateExecutor type TemplateExecutor scopedtmpl.TemplateExecutor
type TplName string
type HTMLRender struct { type HTMLRender struct {
templates atomic.Pointer[scopedtmpl.ScopedTemplate] templates atomic.Pointer[scopedtmpl.ScopedTemplate]
} }
@ -40,7 +42,8 @@ var (
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive
name := string(tplName)
if respWriter, ok := w.(http.ResponseWriter); ok { if respWriter, ok := w.(http.ResponseWriter); ok {
if respWriter.Header().Get("Content-Type") == "" { if respWriter.Header().Get("Content-Type") == "" {
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8") respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")

View File

@ -0,0 +1,37 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"fmt"
"code.gitea.io/gitea/modules/util"
)
func timeEstimateString(timeSec any) string {
v, _ := util.ToInt64(timeSec)
if v == 0 {
return ""
}
return util.TimeEstimateString(v)
}
func countFmt(data any) string {
// legacy code, not ideal, still used in some places
num, err := util.ToInt64(data)
if err != nil {
return ""
}
if num < 1000 {
return fmt.Sprintf("%d", num)
} else if num < 1_000_000 {
num2 := float32(num) / 1000.0
return fmt.Sprintf("%.1fk", num2)
} else if num < 1_000_000_000 {
num2 := float32(num) / 1_000_000.0
return fmt.Sprintf("%.1fM", num2)
}
num2 := float32(num) / 1_000_000_000.0
return fmt.Sprintf("%.1fG", num2)
}

View File

@ -0,0 +1,18 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCountFmt(t *testing.T) {
assert.Equal(t, "125", countFmt(125))
assert.Equal(t, "1.3k", countFmt(int64(1317)))
assert.Equal(t, "21.3M", countFmt(21317675))
assert.Equal(t, "45.7G", countFmt(45721317675))
assert.Equal(t, "", countFmt("test"))
}

View File

@ -60,3 +60,7 @@ func (su *StringUtils) EllipsisString(s string, maxLength int) string {
func (su *StringUtils) ToUpper(s string) string { func (su *StringUtils) ToUpper(s string) string {
return strings.ToUpper(s) return strings.ToUpper(s)
} }
func (su *StringUtils) TrimPrefix(s, prefix string) string {
return strings.TrimPrefix(s, prefix)
}

View File

@ -4,11 +4,16 @@
package test package test
import ( import (
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings" "strings"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
) )
// RedirectURL returns the redirect URL of a http response. // RedirectURL returns the redirect URL of a http response.
@ -41,3 +46,19 @@ func MockVariableValue[T any](p *T, v ...T) (reset func()) {
} }
return func() { *p = old } return func() { *p = old }
} }
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
func SetupGiteaRoot() string {
giteaRoot := os.Getenv("GITEA_ROOT")
if giteaRoot != "" {
return giteaRoot
}
_, filename, _, _ := runtime.Caller(0)
giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
fixturesDir := filepath.Join(giteaRoot, "models", "fixtures")
if exist, _ := util.IsDir(fixturesDir); !exist {
panic(fmt.Sprintf("fixtures directory not found: %s", fixturesDir))
}
_ = os.Setenv("GITEA_ROOT", giteaRoot)
return giteaRoot
}

View File

@ -0,0 +1,17 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package test
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetupGiteaRoot(t *testing.T) {
t.Setenv("GITEA_ROOT", "test")
assert.Equal(t, "test", SetupGiteaRoot())
t.Setenv("GITEA_ROOT", "")
assert.NotEqual(t, "test", SetupGiteaRoot())
}

13
modules/util/runtime.go Normal file
View File

@ -0,0 +1,13 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import "runtime"
func CallerFuncName(skip int) string {
pc := make([]uintptr, 1)
runtime.Callers(skip+1, pc)
funcName := runtime.FuncForPC(pc[0]).Name()
return funcName
}

View File

@ -0,0 +1,32 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCallerFuncName(t *testing.T) {
s := CallerFuncName(1)
assert.Equal(t, "code.gitea.io/gitea/modules/util.TestCallerFuncName", s)
}
func BenchmarkCallerFuncName(b *testing.B) {
// BenchmarkCaller/sprintf-12 12744829 95.49 ns/op
b.Run("sprintf", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("aaaaaaaaaaaaaaaa %s %s %s", "bbbbbbbbbbbbbbbbbbb", b.Name(), "ccccccccccccccccccccc")
}
})
// BenchmarkCaller/caller-12 10625133 113.6 ns/op
// It is almost as fast as fmt.Sprintf
b.Run("caller", func(b *testing.B) {
for i := 0; i < b.N; i++ {
CallerFuncName(1)
}
})
}

View File

@ -25,7 +25,7 @@ type timeStrGlobalVarsType struct {
// In the future, it could be some configurable options to help users // In the future, it could be some configurable options to help users
// to convert the working time to different units. // to convert the working time to different units.
var timeStrGlobalVars = sync.OnceValue[*timeStrGlobalVarsType](func() *timeStrGlobalVarsType { var timeStrGlobalVars = sync.OnceValue(func() *timeStrGlobalVarsType {
v := &timeStrGlobalVarsType{} v := &timeStrGlobalVarsType{}
v.re = regexp.MustCompile(`(?i)(\d+)\s*([hms])`) v.re = regexp.MustCompile(`(?i)(\d+)\s*([hms])`)
v.units = []struct { v.units = []struct {

View File

@ -242,10 +242,10 @@ func TestReserveLineBreakForTextarea(t *testing.T) {
} }
func TestOptionalArg(t *testing.T) { func TestOptionalArg(t *testing.T) {
foo := func(other any, optArg ...int) int { foo := func(_ any, optArg ...int) int {
return OptionalArg(optArg) return OptionalArg(optArg)
} }
bar := func(other any, optArg ...int) int { bar := func(_ any, optArg ...int) int {
return OptionalArg(optArg, 42) return OptionalArg(optArg, 42)
} }
assert.Equal(t, 0, foo(nil)) assert.Equal(t, 0, foo(nil))

View File

@ -0,0 +1,31 @@
# gitignore template for B&R Automation Studio (AS) 4
# website: https://www.br-automation.com/en-us/products/software/automation-software/automation-studio/
# AS temporary directories
Binaries/
Diagnosis/
Temp/
TempObjects/
# AS transfer files
*artransfer.br
*arTrsfmode.nv
# 'ignored' directory
ignored/
# ARNC0ext
*arnc0ext.br
# AS File types
*.bak
*.isopen
*.orig
*.log
*.asar
*.csvlog*
*.set
!**/Physical/**/*.set
# RevInfo variables
*RevInfo.var

View File

@ -0,0 +1,28 @@
# Firebase build and deployment files
/firebase-debug.log
/firebase-debug.*.log
.firebaserc
# Firebase Hosting
/firebase.json
*.cache
hosting/.cache
# Firebase Functions
/functions/node_modules/
/functions/.env
/functions/package-lock.json
# Firebase Emulators
/firebase-*.zip
/.firebase/
/emulator-ui/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files (local configs)
/.env.*

View File

@ -0,0 +1,42 @@
# Modelica - an object-oriented language for modeling of cyber-physical systems
# https://modelica.org/
# Ignore temporary files, build results, simulation files
## Modelica-specific files
*~
*.bak
*.bak-mo
*.mof
\#*\#
*.moe
*.mol
## Build artefacts
*.exe
*.exp
*.o
*.pyc
## Simulation files
*.mat
## Package files
*.gz
*.rar
*.tar
*.zip
## Dymola-specific files
buildlog.txt
dsfinal.txt
dsin.txt
dslog.txt
dsmodel*
dsres.txt
dymosim*
request
stat
status
stop
success
*.

View File

@ -166,3 +166,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# PyPI configuration file
.pypirc

View File

@ -26,6 +26,7 @@
## Bibliography auxiliary files (bibtex/biblatex/biber): ## Bibliography auxiliary files (bibtex/biblatex/biber):
*.bbl *.bbl
*.bbl-SAVE-ERROR
*.bcf *.bcf
*.blg *.blg
*-blx.aux *-blx.aux

View File

@ -2,18 +2,18 @@ Elastic License 2.0
URL: https://www.elastic.co/licensing/elastic-license URL: https://www.elastic.co/licensing/elastic-license
## Acceptance Acceptance
By using the software, you agree to all of the terms and conditions below. By using the software, you agree to all of the terms and conditions below.
## Copyright License Copyright License
The licensor grants you a non-exclusive, royalty-free, worldwide, The licensor grants you a non-exclusive, royalty-free, worldwide,
non-sublicensable, non-transferable license to use, copy, distribute, make non-sublicensable, non-transferable license to use, copy, distribute, make
available, and prepare derivative works of the software, in each case subject to available, and prepare derivative works of the software, in each case subject to
the limitations and conditions below. the limitations and conditions below.
## Limitations Limitations
You may not provide the software to third parties as a hosted or managed You may not provide the software to third parties as a hosted or managed
service, where the service provides users with access to any substantial set of service, where the service provides users with access to any substantial set of
@ -27,7 +27,7 @@ You may not alter, remove, or obscure any licensing, copyright, or other notices
of the licensor in the software. Any use of the licensors trademarks is subject of the licensor in the software. Any use of the licensors trademarks is subject
to applicable law. to applicable law.
## Patents Patents
The licensor grants you a license, under any patent claims the licensor can The licensor grants you a license, under any patent claims the licensor can
license, or becomes able to license, to make, have made, use, sell, offer for license, or becomes able to license, to make, have made, use, sell, offer for
@ -40,7 +40,7 @@ the software granted under these terms ends immediately. If your company makes
such a claim, your patent license ends immediately for work on behalf of your such a claim, your patent license ends immediately for work on behalf of your
company. company.
## Notices Notices
You must ensure that anyone who gets a copy of any part of the software from you You must ensure that anyone who gets a copy of any part of the software from you
also gets a copy of these terms. also gets a copy of these terms.
@ -53,7 +53,7 @@ software prominent notices stating that you have modified the software.
These terms do not imply any licenses other than those expressly granted in These terms do not imply any licenses other than those expressly granted in
these terms. these terms.
## Termination Termination
If you use the software in violation of these terms, such use is not licensed, If you use the software in violation of these terms, such use is not licensed,
and your licenses will automatically terminate. If the licensor provides you and your licenses will automatically terminate. If the licensor provides you
@ -63,31 +63,31 @@ reinstated retroactively. However, if you violate these terms after such
reinstatement, any additional violation of these terms will cause your licenses reinstatement, any additional violation of these terms will cause your licenses
to terminate automatically and permanently. to terminate automatically and permanently.
## No Liability No Liability
*As far as the law allows, the software comes as is, without any warranty or As far as the law allows, the software comes as is, without any warranty or
condition, and the licensor will not be liable to you for any damages arising condition, and the licensor will not be liable to you for any damages arising
out of these terms or the use or nature of the software, under any kind of out of these terms or the use or nature of the software, under any kind of
legal claim.* legal claim.
## Definitions Definitions
The **licensor** is the entity offering these terms, and the **software** is the The licensor is the entity offering these terms, and the software is the
software the licensor makes available under these terms, including any portion software the licensor makes available under these terms, including any portion
of it. of it.
**you** refers to the individual or entity agreeing to these terms. you refers to the individual or entity agreeing to these terms.
**your company** is any legal entity, sole proprietorship, or other kind of your company is any legal entity, sole proprietorship, or other kind of
organization that you work for, plus all organizations that have control over, organization that you work for, plus all organizations that have control over,
are under the control of, or are under common control with that are under the control of, or are under common control with that
organization. **control** means ownership of substantially all the assets of an organization. control means ownership of substantially all the assets of an
entity, or the power to direct its management and policies by vote, contract, or entity, or the power to direct its management and policies by vote, contract, or
otherwise. Control can be direct or indirect. otherwise. Control can be direct or indirect.
**your licenses** are all the licenses granted to you for the software under your licenses are all the licenses granted to you for the software under
these terms. these terms.
**use** means anything you do with the software requiring one of your licenses. use means anything you do with the software requiring one of your licenses.
**trademark** means trademarks, service marks, and similar rights. trademark means trademarks, service marks, and similar rights.

4
options/license/MIPS Normal file
View File

@ -0,0 +1,4 @@
Copyright (c) 1992, 1991, 1990 MIPS Computer Systems, Inc.
MIPS Computer Systems, Inc. grants reproduction and use
rights to all parties, PROVIDED that this comment is
maintained in the copy.

View File

@ -2,8 +2,17 @@ MIT License
Copyright (c) <year> <copyright holders> Copyright (c) <year> <copyright holders>
Permission 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: Permission 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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE 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. THE 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.

7
options/license/ThirdEye Normal file
View File

@ -0,0 +1,7 @@
(C) Copyright 1984 by Third Eye Software, Inc.
Third Eye Software, Inc. grants reproduction and use rights to
all parties, PROVIDED that this comment is maintained in the copy.
Third Eye makes no claims about the applicability of this
symbol table to a particular use.

View File

@ -1946,8 +1946,8 @@ pulls.delete.title = Delete this pull request?
pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong> %[2]s pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong> %[2]s
pulls.upstream_diverging_prompt_behind_1 = This branch is %d commit behind %s pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[2]s
pulls.upstream_diverging_prompt_behind_n = This branch is %d commits behind %s pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
pulls.upstream_diverging_merge = Sync fork pulls.upstream_diverging_merge = Sync fork
@ -3773,6 +3773,9 @@ variables.creation.success = The variable "%s" has been added.
variables.update.failed = Failed to edit variable. variables.update.failed = Failed to edit variable.
variables.update.success = The variable has been edited. variables.update.success = The variable has been edited.
logs.always_auto_scroll = Always auto scroll logs
logs.always_expand_running = Always expand running logs
[projects] [projects]
deleted.display_name = Deleted Project deleted.display_name = Deleted Project
type-1.display_name = Individual Project type-1.display_name = Individual Project

View File

@ -1109,6 +1109,7 @@ delete_preexisting_success=Fichiers dépossédés supprimés dans %s.
blame_prior=Voir le blame avant cette modification blame_prior=Voir le blame avant cette modification
blame.ignore_revs=Les révisions dans <a href="%s">.git-blame-ignore-revs</a> sont ignorées. Vous pouvez quand même <a href="%s">voir ces blâmes</a>. blame.ignore_revs=Les révisions dans <a href="%s">.git-blame-ignore-revs</a> sont ignorées. Vous pouvez quand même <a href="%s">voir ces blâmes</a>.
blame.ignore_revs.failed=Impossible d'ignorer les révisions dans <a href="%s">.git-blame-ignore-revs</a>. blame.ignore_revs.failed=Impossible d'ignorer les révisions dans <a href="%s">.git-blame-ignore-revs</a>.
user_search_tooltip=Affiche un maximum de 30 utilisateurs
tree_path_not_found_commit=Le chemin %[1]s nexiste pas dans la révision %[2]s. tree_path_not_found_commit=Le chemin %[1]s nexiste pas dans la révision %[2]s.
tree_path_not_found_branch=Le chemin %[1]s nexiste pas dans la branche %[2]s. tree_path_not_found_branch=Le chemin %[1]s nexiste pas dans la branche %[2]s.
@ -1527,6 +1528,8 @@ issues.filter_assignee=Assigné
issues.filter_assginee_no_select=Tous les assignés issues.filter_assginee_no_select=Tous les assignés
issues.filter_assginee_no_assignee=Aucun assigné issues.filter_assginee_no_assignee=Aucun assigné
issues.filter_poster=Auteur issues.filter_poster=Auteur
issues.filter_user_placeholder=Rechercher des utilisateurs
issues.filter_user_no_select=Tous les utilisateurs
issues.filter_type=Type issues.filter_type=Type
issues.filter_type.all_issues=Tous les tickets issues.filter_type.all_issues=Tous les tickets
issues.filter_type.assigned_to_you=Qui vous sont assignés issues.filter_type.assigned_to_you=Qui vous sont assignés
@ -1676,7 +1679,6 @@ issues.timetracker_timer_stop=Arrêter le minuteur
issues.timetracker_timer_discard=Annuler le minuteur issues.timetracker_timer_discard=Annuler le minuteur
issues.timetracker_timer_manually_add=Pointer du temps issues.timetracker_timer_manually_add=Pointer du temps
issues.time_estimate_placeholder=1h 2m
issues.time_estimate_set=Définir le temps estimé issues.time_estimate_set=Définir le temps estimé
issues.time_estimate_display=Estimation : %s issues.time_estimate_display=Estimation : %s
issues.change_time_estimate_at=a changé le temps estimé à <b>%s</b> %s issues.change_time_estimate_at=a changé le temps estimé à <b>%s</b> %s
@ -1943,8 +1945,6 @@ pulls.delete.title=Supprimer cette demande d'ajout ?
pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela supprimera définitivement tout le contenu. Envisagez de le fermer à la place, si vous avez l'intention de le garder archivé) pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela supprimera définitivement tout le contenu. Envisagez de le fermer à la place, si vous avez l'intention de le garder archivé)
pulls.recently_pushed_new_branches=Vous avez soumis sur la branche <strong>%[1]s</strong> %[2]s pulls.recently_pushed_new_branches=Vous avez soumis sur la branche <strong>%[1]s</strong> %[2]s
pulls.upstream_diverging_prompt_behind_1=Cette branche est en retard de %d révision sur %s
pulls.upstream_diverging_prompt_behind_n=Cette branche est en retard de %d révisions sur %s
pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements
pulls.upstream_diverging_merge=Synchroniser la bifurcation pulls.upstream_diverging_merge=Synchroniser la bifurcation
@ -2629,6 +2629,7 @@ release.new_release=Nouvelle publication
release.draft=Brouillon release.draft=Brouillon
release.prerelease=Pré-publication release.prerelease=Pré-publication
release.stable=Stable release.stable=Stable
release.latest=Dernière
release.compare=Comparer release.compare=Comparer
release.edit=Éditer release.edit=Éditer
release.ahead.commits=<strong>%d</strong> révisions release.ahead.commits=<strong>%d</strong> révisions

View File

@ -1109,6 +1109,7 @@ delete_preexisting_success=Scriosta comhaid neamhghlactha i %s
blame_prior=Féach ar an milleán roimh an athrú seo blame_prior=Féach ar an milleán roimh an athrú seo
blame.ignore_revs=Ag déanamh neamhairde de leasuithe i <a href="%s">.git-blame-ignore-revs</a>. Cliceáil <a href="%s">anseo chun seachaint</a> agus an gnáth-amharc milleán a fheiceáil. blame.ignore_revs=Ag déanamh neamhairde de leasuithe i <a href="%s">.git-blame-ignore-revs</a>. Cliceáil <a href="%s">anseo chun seachaint</a> agus an gnáth-amharc milleán a fheiceáil.
blame.ignore_revs.failed=Theip ar neamhaird a dhéanamh ar leasuithe i <a href="%s">.git-blame-ignore-revs</a>. blame.ignore_revs.failed=Theip ar neamhaird a dhéanamh ar leasuithe i <a href="%s">.git-blame-ignore-revs</a>.
user_search_tooltip=Taispeáint uasmhéid de 30 úsáideoir
tree_path_not_found_commit=Níl cosán %[1]s ann i dtiomantas %[2]s tree_path_not_found_commit=Níl cosán %[1]s ann i dtiomantas %[2]s
tree_path_not_found_branch=Níl cosán %[1]s ann i mbrainse %[2]s tree_path_not_found_branch=Níl cosán %[1]s ann i mbrainse %[2]s
@ -1527,6 +1528,8 @@ issues.filter_assignee=Sannaitheoir
issues.filter_assginee_no_select=Gach sannaithe issues.filter_assginee_no_select=Gach sannaithe
issues.filter_assginee_no_assignee=Gan sannaitheoir issues.filter_assginee_no_assignee=Gan sannaitheoir
issues.filter_poster=Údar issues.filter_poster=Údar
issues.filter_user_placeholder=Cuardaigh úsáideoirí
issues.filter_user_no_select=Gach úsáideoir
issues.filter_type=Cineál issues.filter_type=Cineál
issues.filter_type.all_issues=Gach saincheist issues.filter_type.all_issues=Gach saincheist
issues.filter_type.assigned_to_you=Sannta duit issues.filter_type.assigned_to_you=Sannta duit
@ -1676,7 +1679,6 @@ issues.timetracker_timer_stop=Stop an t-amadóir
issues.timetracker_timer_discard=Déan an t-amadóir a scriosadh issues.timetracker_timer_discard=Déan an t-amadóir a scriosadh
issues.timetracker_timer_manually_add=Cuir Am leis issues.timetracker_timer_manually_add=Cuir Am leis
issues.time_estimate_placeholder=1u 2n
issues.time_estimate_set=Socraigh am measta issues.time_estimate_set=Socraigh am measta
issues.time_estimate_display=Meastachán: %s issues.time_estimate_display=Meastachán: %s
issues.change_time_estimate_at=d'athraigh an meastachán ama go <b>%s</b> %s issues.change_time_estimate_at=d'athraigh an meastachán ama go <b>%s</b> %s
@ -1943,8 +1945,6 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo?
pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann) pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann)
pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s
pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %d tiomantas taobh thiar de %s
pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %d geallta taobh thiar de %s
pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s
pulls.upstream_diverging_merge=Forc sionc pulls.upstream_diverging_merge=Forc sionc
@ -2629,6 +2629,7 @@ release.new_release=Scaoileadh Nua
release.draft=Dréacht release.draft=Dréacht
release.prerelease=Réamh-eisiúint release.prerelease=Réamh-eisiúint
release.stable=Cobhsaí release.stable=Cobhsaí
release.latest=Is déanaí
release.compare=Déan comparáid release.compare=Déan comparáid
release.edit=cuir in eagar release.edit=cuir in eagar
release.ahead.commits=Geallann <strong>%d</strong> release.ahead.commits=Geallann <strong>%d</strong>

View File

@ -22,11 +22,25 @@ toc=Daftar Isi
username=Nama Pengguna username=Nama Pengguna
email=Alamat Email email=Alamat Email
password=Kata Sandi password=Kata Sandi
re_type=Konfirmasi Kata Sandi
captcha=CAPTCHA captcha=CAPTCHA
twofa=Otentikasi Dua Faktor twofa=Otentikasi Dua Faktor
twofa_scratch=Kode Awal Dua Faktor twofa_scratch=Kode Awal Dua Faktor
passcode=Kode Akses passcode=Kode Akses
webauthn_insert_key=Masukkan kunci keamanan anda
webauthn_sign_in=Tekan tombol pada kunci keamanan Anda. Jika kunci keamanan Anda tidak memiliki tombol, masukkan kembali.
webauthn_press_button=Silakan tekan tombol pada kunci keamanan Anda…
webauthn_use_twofa=Gunakan kode dua faktor dari telepon Anda
webauthn_error=Tidak dapat membaca kunci keamanan Anda.
webauthn_unsupported_browser=Browser Anda saat ini tidak mendukung WebAuthn.
webauthn_error_unknown=Terdapat kesalahan yang tidak diketahui. Mohon coba lagi.
webauthn_error_insecure=`WebAuthn hanya mendukung koneksi aman. Untuk pengujian melalui HTTP, Anda dapat menggunakan "localhost" atau "127.0.0.1"`
webauthn_error_unable_to_process=Server tidak dapat memproses permintaan Anda.
webauthn_error_duplicated=Kunci keamanan tidak diperbolehkan untuk permintaan ini. Pastikan bahwa kunci ini belum terdaftar sebelumnya.
webauthn_error_empty=Anda harus menetapkan nama untuk kunci ini.
webauthn_error_timeout=Waktu habis sebelum kunci Anda dapat dibaca. Mohon muat ulang halaman ini dan coba lagi.
webauthn_reload=Muat ulang
repository=Repositori repository=Repositori
organization=Organisasi organization=Organisasi
@ -36,6 +50,8 @@ new_migrate=Migrasi Baru
new_mirror=Duplikat Baru new_mirror=Duplikat Baru
new_fork=Fork Repositori Baru new_fork=Fork Repositori Baru
new_org=Organisasi Baru new_org=Organisasi Baru
new_project=Proyek Baru
new_project_column=Kolom Baru
manage_org=Mengelola Organisasi manage_org=Mengelola Organisasi
admin_panel=Administrasi Situs admin_panel=Administrasi Situs
account_settings=Pengaturan Akun account_settings=Pengaturan Akun
@ -55,29 +71,58 @@ pull_requests=Tarik Permintaan
issues=Masalah issues=Masalah
milestones=Tonggak milestones=Tonggak
ok=Oke
cancel=Batal cancel=Batal
retry=Coba lagi
rerun=Jalankan ulang
rerun_all=Jalankan ulang semua job
save=Simpan save=Simpan
add=Tambah add=Tambah
add_all=Tambah Semua add_all=Tambah Semua
remove=Buang remove=Buang
remove_all=Buang Semua remove_all=Buang Semua
remove_label_str=`Hapus item "%s"`
edit=Edit edit=Edit
view=Tampilan
test=Pengujian
enabled=Aktif enabled=Aktif
disabled=Nonaktif disabled=Nonaktif
locked=Terkunci
copy=Salin
copy_url=Salin URL
copy_hash=Salin hash
copy_content=Salin konten
copy_branch=Salin nama branch
copy_success=Tersalin!
copy_error=Gagal menyalin
copy_type_unsupported=Tipe berkas ini tidak dapat disalin
write=Tulis write=Tulis
preview=Pratinjau preview=Pratinjau
loading=Memuat… loading=Memuat…
error=Gangguan
error404=Halaman yang akan kamu akses <strong>tidak dapat ditemukan</strong> atau <strong>kamu tidak memiliki akses </strong> untuk melihatnya.
go_back=Kembali
invalid_data=Data invalid: %v
never=Tidak Pernah
unknown=Tidak diketahui
rss_feed=Umpan Berita
pin=Sematkan
unpin=Lepas sematan
artifacts=Artefak
confirm_delete_artifact=Apakah Anda yakin ingin menghapus artefak '%s' ?
archived=Diarsipkan archived=Diarsipkan
concept_system_global=Global
concept_user_individual=Perorangan
concept_code_repository=Repositori concept_code_repository=Repositori
show_full_screen=Tampilkan layar penuh show_full_screen=Tampilkan layar penuh
@ -682,13 +727,16 @@ commits.newer=Terbaru
commits.signed_by=Ditandai oleh commits.signed_by=Ditandai oleh
commitstatus.error=Gangguan
projects.description_placeholder=Deskripsi projects.description_placeholder=Deskripsi
projects.title=Judul projects.title=Judul
projects.new=Proyek Baru
projects.template.desc=Contoh projects.template.desc=Contoh
projects.column.edit_title=Nama projects.column.edit_title=Nama
projects.column.new_title=Nama projects.column.new_title=Nama
projects.column.new=Kolom Baru
issues.new=Masalah Baru issues.new=Masalah Baru
issues.new.labels=Label issues.new.labels=Label

View File

@ -145,6 +145,7 @@ confirm_delete_selected=選択したすべてのアイテムを削除してよ
name=名称 name=名称
value= value=
readme=Readme
filter=フィルター filter=フィルター
filter.clear=フィルターをクリア filter.clear=フィルターをクリア
@ -1043,6 +1044,8 @@ generate_repo=リポジトリの生成
generate_from=他からの生成 generate_from=他からの生成
repo_desc=説明 repo_desc=説明
repo_desc_helper=簡単な説明を入力してください (オプション) repo_desc_helper=簡単な説明を入力してください (オプション)
repo_no_desc=説明が提供されていません
repo_lang=言語
repo_gitignore_helper=.gitignoreテンプレートを選択してください。 repo_gitignore_helper=.gitignoreテンプレートを選択してください。
repo_gitignore_helper_desc=一般的な言語のテンプレートリストから、追跡しないファイルの設定を選択します。 各言語のビルドツールが生成する典型的なファイルが、デフォルトで.gitignoreに含まれます。 repo_gitignore_helper_desc=一般的な言語のテンプレートリストから、追跡しないファイルの設定を選択します。 各言語のビルドツールが生成する典型的なファイルが、デフォルトで.gitignoreに含まれます。
issue_labels=イシューラベル issue_labels=イシューラベル
@ -1666,12 +1669,25 @@ issues.delete.title=このイシューを削除しますか?
issues.delete.text=本当にこのイシューを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください) issues.delete.text=本当にこのイシューを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください)
issues.tracker=タイムトラッカー issues.tracker=タイムトラッカー
issues.timetracker_timer_start=タイマー開始
issues.timetracker_timer_stop=タイマー終了
issues.timetracker_timer_discard=タイマー破棄
issues.timetracker_timer_manually_add=時間を追加
issues.time_estimate_set=見積時間を設定
issues.time_estimate_display=見積時間: %s
issues.change_time_estimate_at=が見積時間を <b>%s</b> に変更 %s
issues.remove_time_estimate_at=が見積時間を削除 %s
issues.time_estimate_invalid=見積時間のフォーマットが不正です
issues.start_tracking_history=が作業を開始 %s
issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します
issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!` issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!`
issues.stop_tracking_history=が <b>%s</b> の作業を終了 %s
issues.cancel_tracking_history=`がタイムトラッキングを中止 %s` issues.cancel_tracking_history=`がタイムトラッキングを中止 %s`
issues.del_time=このタイムログを削除 issues.del_time=このタイムログを削除
issues.add_time_history=が作業時間 <b>%s</b> を追加 %s
issues.del_time_history=`が作業時間を削除 %s` issues.del_time_history=`が作業時間を削除 %s`
issues.add_time_manually=時間の手入力
issues.add_time_hours=時間 issues.add_time_hours=時間
issues.add_time_minutes= issues.add_time_minutes=
issues.add_time_sum_to_small=時間が入力されていません。 issues.add_time_sum_to_small=時間が入力されていません。
@ -1691,15 +1707,15 @@ issues.due_date_form_add=期日の追加
issues.due_date_form_edit=変更 issues.due_date_form_edit=変更
issues.due_date_form_remove=削除 issues.due_date_form_remove=削除
issues.due_date_not_writer=イシューの期日を変更するには、リポジトリへの書き込み権限が必要です。 issues.due_date_not_writer=イシューの期日を変更するには、リポジトリへの書き込み権限が必要です。
issues.due_date_not_set=期日は未設定です issues.due_date_not_set=期日は設定されていません
issues.due_date_added=が期日 %s を追加 %s issues.due_date_added=が期日 %s を追加 %s
issues.due_date_modified=が期日を %[2]s から %[1]s に変更 %[3]s issues.due_date_modified=が期日を %[2]s から %[1]s に変更 %[3]s
issues.due_date_remove=が期日 %s を削除 %s issues.due_date_remove=が期日 %s を削除 %s
issues.due_date_overdue=期日は過ぎています issues.due_date_overdue=期日は過ぎています
issues.due_date_invalid=期日が正しくないか範囲を超えています。 'yyyy-mm-dd' の形式で入力してください。 issues.due_date_invalid=期日が正しくないか範囲を超えています。 'yyyy-mm-dd' の形式で入力してください。
issues.dependency.title=依存関係 issues.dependency.title=依存関係
issues.dependency.issue_no_dependencies=依存関係設定されていません。 issues.dependency.issue_no_dependencies=依存関係設定されていません。
issues.dependency.pr_no_dependencies=依存関係設定されていません。 issues.dependency.pr_no_dependencies=依存関係設定されていません。
issues.dependency.no_permission_1=%d 個の依存関係への読み取り権限がありません issues.dependency.no_permission_1=%d 個の依存関係への読み取り権限がありません
issues.dependency.no_permission_n=%d 個の依存関係への読み取り権限がありません issues.dependency.no_permission_n=%d 個の依存関係への読み取り権限がありません
issues.dependency.no_permission.can_remove=この依存関係への読み取り権限はありませんが、この依存関係は削除できます issues.dependency.no_permission.can_remove=この依存関係への読み取り権限はありませんが、この依存関係は削除できます
@ -1924,6 +1940,8 @@ pulls.delete.title=このプルリクエストを削除しますか?
pulls.delete.text=本当にこのプルリクエストを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください) pulls.delete.text=本当にこのプルリクエストを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください)
pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1]s</strong> にプッシュしました pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1]s</strong> にプッシュしました
pulls.upstream_diverging_prompt_base_newer=ベースブランチ %s に新しい変更があります
pulls.upstream_diverging_merge=フォークを同期
pull.deleted_branch=(削除済み):%s pull.deleted_branch=(削除済み):%s
pull.agit_documentation=AGitに関するドキュメントを確認する pull.agit_documentation=AGitに関するドキュメントを確認する

View File

@ -1679,7 +1679,6 @@ issues.timetracker_timer_stop=Parar cronómetro
issues.timetracker_timer_discard=Descartar cronómetro issues.timetracker_timer_discard=Descartar cronómetro
issues.timetracker_timer_manually_add=Adicionar tempo issues.timetracker_timer_manually_add=Adicionar tempo
issues.time_estimate_placeholder=1h 2m
issues.time_estimate_set=Definir tempo estimado issues.time_estimate_set=Definir tempo estimado
issues.time_estimate_display=Estimativa: %s issues.time_estimate_display=Estimativa: %s
issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%s</b> %s issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%s</b> %s
@ -1946,8 +1945,6 @@ pulls.delete.title=Eliminar este pedido de integração?
pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo. pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo.
pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2]s pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2]s
pulls.upstream_diverging_prompt_behind_1=Este ramo está %d cometimento atrás de %s
pulls.upstream_diverging_prompt_behind_n=Este ramo está %d cometimentos atrás de %s
pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações
pulls.upstream_diverging_merge=Sincronizar derivação pulls.upstream_diverging_merge=Sincronizar derivação
@ -2632,6 +2629,7 @@ release.new_release=Novo lançamento
release.draft=Rascunho release.draft=Rascunho
release.prerelease=Pré-lançamento release.prerelease=Pré-lançamento
release.stable=Estável release.stable=Estável
release.latest=Mais recente
release.compare=Comparar release.compare=Comparar
release.edit=editar release.edit=editar
release.ahead.commits=<strong>%d</strong> cometimentos release.ahead.commits=<strong>%d</strong> cometimentos

View File

@ -93,6 +93,7 @@ remove_all=移除所有
remove_label_str=`删除标签 "%s"` remove_label_str=`删除标签 "%s"`
edit=编辑 edit=编辑
view=查看 view=查看
test=测试
enabled=启用 enabled=启用
disabled=禁用 disabled=禁用
@ -103,6 +104,7 @@ copy_url=复制网址
copy_hash=复制哈希值 copy_hash=复制哈希值
copy_content=复制内容 copy_content=复制内容
copy_branch=复制分支名 copy_branch=复制分支名
copy_path=复制路径
copy_success=复制成功! copy_success=复制成功!
copy_error=复制失败 copy_error=复制失败
copy_type_unsupported=无法复制此类型的文件内容 copy_type_unsupported=无法复制此类型的文件内容
@ -143,6 +145,7 @@ confirm_delete_selected=确认删除所有选中项目?
name=名称 name=名称
value= value=
readme=自述文档
filter=过滤 filter=过滤
filter.clear=清除筛选器 filter.clear=清除筛选器
@ -178,6 +181,7 @@ package_kind=搜索软件包...
project_kind=搜索项目... project_kind=搜索项目...
branch_kind=搜索分支... branch_kind=搜索分支...
tag_kind=搜索标签... tag_kind=搜索标签...
tag_tooltip=搜索匹配的标签。使用“%”来匹配任何序列的数字
commit_kind=搜索提交记录... commit_kind=搜索提交记录...
runner_kind=搜索runners... runner_kind=搜索runners...
no_results=未找到匹配结果 no_results=未找到匹配结果
@ -223,16 +227,20 @@ string.desc=Z - A
[error] [error]
occurred=发生了一个错误 occurred=发生了一个错误
report_message=如果您确定这是一个 Gitea bug请在 <a href="%s" target="_blank">这里</a> 搜索问题,或在必要时创建一个新工单。
not_found=找不到目标。 not_found=找不到目标。
network_error=网络错误 network_error=网络错误
[startpage] [startpage]
app_desc=一款极易搭建的自助 Git 服务 app_desc=一款极易搭建的自助 Git 服务
install=易安装 install=易安装
install_desc=通过 <a target="_blank" rel="noopener noreferrer" href="%[1]s">二进制</a> 来运行;或者通过 <a target="_blank" rel="noopener noreferrer" href="%[2]s">Docker</a> 来运行;或者通过 <a target="_blank" rel="noopener noreferrer" href="%[3]s">安装包</a> 来运行。
platform=跨平台 platform=跨平台
platform_desc=任何 <a target="_blank" rel="noopener noreferrer" href="%s">Go 语言</a> 支持的平台都可以运行 Gitea包括 Windows、Mac、Linux 以及 ARM。挑一个您喜欢的就行
lightweight=轻量级 lightweight=轻量级
lightweight_desc=一个廉价的树莓派的配置足以满足 Gitea 的最低系统硬件要求。最大程度上节省您的服务器资源! lightweight_desc=一个廉价的树莓派的配置足以满足 Gitea 的最低系统硬件要求。最大程度上节省您的服务器资源!
license=开源化 license=开源化
license_desc=所有的代码都开源在 <a target="_blank" rel="noopener noreferrer" href="%[1]s">%[2]s</a> 上,赶快加入我们来<a target="_blank" rel="noopener noreferrer" href="%[3]s">共同发展</a>这个伟大的项目!还等什么?成为贡献者吧!
[install] [install]
install=安装页面 install=安装页面
@ -346,6 +354,7 @@ enable_update_checker=启用更新检查
enable_update_checker_helper=通过连接到 gitea.io 定期检查新版本发布。 enable_update_checker_helper=通过连接到 gitea.io 定期检查新版本发布。
env_config_keys=环境配置 env_config_keys=环境配置
env_config_keys_prompt=以下环境变量也将应用于您的配置文件: env_config_keys_prompt=以下环境变量也将应用于您的配置文件:
config_write_file_prompt=这些配置选项将写入以下位置: %s
[home] [home]
nav_menu=导航菜单 nav_menu=导航菜单
@ -386,6 +395,7 @@ relevant_repositories=只显示相关的仓库, <a href="%s">显示未过滤
[auth] [auth]
create_new_account=注册帐号 create_new_account=注册帐号
already_have_account=已有账号?
sign_in_now=立即登录 sign_in_now=立即登录
disable_register_prompt=对不起,注册功能已被关闭。请联系网站管理员。 disable_register_prompt=对不起,注册功能已被关闭。请联系网站管理员。
disable_register_mail=已禁用注册的电子邮件确认。 disable_register_mail=已禁用注册的电子邮件确认。
@ -394,6 +404,7 @@ remember_me=记住此设备
remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。 remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。
forgot_password_title=忘记密码 forgot_password_title=忘记密码
forgot_password=忘记密码? forgot_password=忘记密码?
need_account=需要一个帐户?
sign_up_now=还没账号?马上注册。 sign_up_now=还没账号?马上注册。
sign_up_successful=帐户创建成功。欢迎! sign_up_successful=帐户创建成功。欢迎!
confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>请在下一个 %s 中检查您的收件箱以完成注册过程。 如果您的注册电子邮件地址不正确,您可以重新登录并更改它。 confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>请在下一个 %s 中检查您的收件箱以完成注册过程。 如果您的注册电子邮件地址不正确,您可以重新登录并更改它。
@ -449,12 +460,15 @@ authorize_application=应用授权
authorize_redirect_notice=如果您授权此应用,您将会被重定向到 %s。 authorize_redirect_notice=如果您授权此应用,您将会被重定向到 %s。
authorize_application_created_by=此应用由%s创建。 authorize_application_created_by=此应用由%s创建。
authorize_application_description=如果您允许,它将能够读取和修改您的所有帐户信息,包括私人仓库和组织。 authorize_application_description=如果您允许,它将能够读取和修改您的所有帐户信息,包括私人仓库和组织。
authorize_application_with_scopes=范围: %s
authorize_title=授权 %s 访问您的帐户? authorize_title=授权 %s 访问您的帐户?
authorization_failed=授权失败 authorization_failed=授权失败
authorization_failed_desc=因为检测到无效请求,授权失败。请尝试联系您授权应用的管理员。 authorization_failed_desc=因为检测到无效请求,授权失败。请尝试联系您授权应用的管理员。
sspi_auth_failed=SSPI 认证失败 sspi_auth_failed=SSPI 认证失败
password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="%s">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。
password_pwned_err=无法完成对 HaveIBeenPwned 的请求 password_pwned_err=无法完成对 HaveIBeenPwned 的请求
last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。 last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
signin_passkey=使用密钥登录
back_to_sign_in=返回登录页面 back_to_sign_in=返回登录页面
[mail] [mail]
@ -574,6 +588,8 @@ lang_select_error=从列表中选出语言
username_been_taken=用户名已被使用。 username_been_taken=用户名已被使用。
username_change_not_local_user=非本地用户不允许更改用户名。 username_change_not_local_user=非本地用户不允许更改用户名。
change_username_disabled=更改用户名已被禁用。
change_full_name_disabled=更改用户全名已禁用
username_has_not_been_changed=用户名未更改 username_has_not_been_changed=用户名未更改
repo_name_been_taken=仓库名称已被使用。 repo_name_been_taken=仓库名称已被使用。
repository_force_private=“强制私有”已启用:私有仓库不能被公开。 repository_force_private=“强制私有”已启用:私有仓库不能被公开。
@ -623,6 +639,7 @@ org_still_own_repo=该组织仍然是某些仓库的拥有者,请先删除或
org_still_own_packages=该组织仍然是一个或多个软件包的拥有者,请先删除它们。 org_still_own_packages=该组织仍然是一个或多个软件包的拥有者,请先删除它们。
target_branch_not_exist=目标分支不存在。 target_branch_not_exist=目标分支不存在。
target_ref_not_exist=目标引用 %s 不存在
admin_cannot_delete_self=当您是管理员时,您不能删除自己。请先移除您的管理员权限 admin_cannot_delete_self=当您是管理员时,您不能删除自己。请先移除您的管理员权限
@ -688,14 +705,18 @@ applications=应用
orgs=管理组织 orgs=管理组织
repos=仓库列表 repos=仓库列表
delete=删除帐户 delete=删除帐户
twofa=两步验证TOTP
account_link=已绑定帐户 account_link=已绑定帐户
organization=组织 organization=组织
uid=UID uid=UID
webauthn=两步验证(安全密钥)
public_profile=公开信息 public_profile=公开信息
biography_placeholder=告诉我们一点您自己! (您可以使用Markdown) biography_placeholder=告诉我们一点您自己! (您可以使用Markdown)
location_placeholder=与他人分享你的大概位置 location_placeholder=与他人分享你的大概位置
profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作 profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作
password_username_disabled=不允许非本地用户更改他们的用户名。更多详情请联系您的系统管理员。
password_full_name_disabled=您无权更改他们的全名。请联系您的站点管理员了解更多详情。
full_name=自定义名称 full_name=自定义名称
website=个人网站 website=个人网站
location=所在地区 location=所在地区
@ -745,6 +766,7 @@ uploaded_avatar_not_a_image=上传的文件不是一张图片。
uploaded_avatar_is_too_big=上传的文件大小(%d KiB) 超过最大限制(%d KiB)。 uploaded_avatar_is_too_big=上传的文件大小(%d KiB) 超过最大限制(%d KiB)。
update_avatar_success=您的头像已更新。 update_avatar_success=您的头像已更新。
update_user_avatar_success=用户头像已更新。 update_user_avatar_success=用户头像已更新。
cropper_prompt=您可以在保存前编辑图像。编辑的图像将被保存为 PNG 格式。
change_password=更新密码 change_password=更新密码
old_password=当前密码 old_password=当前密码
@ -787,6 +809,7 @@ add_email_success=新的电子邮件地址已添加。
email_preference_set_success=电子邮件首选项已成功设置。 email_preference_set_success=电子邮件首选项已成功设置。
add_openid_success=新的 OpenID 地址已添加。 add_openid_success=新的 OpenID 地址已添加。
keep_email_private=隐藏电子邮件地址 keep_email_private=隐藏电子邮件地址
keep_email_private_popup=这将会隐藏您的电子邮件地址不仅在您的个人资料中还在您使用Web界面创建合并请求或编辑文件时。已推送的提交将不会被修改。在提交中使用 %s 以和您的账号关联。
openid_desc=OpenID 让你可以将认证转发到外部服务。 openid_desc=OpenID 让你可以将认证转发到外部服务。
manage_ssh_keys=管理 SSH 密钥 manage_ssh_keys=管理 SSH 密钥
@ -905,6 +928,7 @@ create_oauth2_application_success=您已成功创建了一个新的 OAuth2 应
update_oauth2_application_success=您已成功更新了此 OAuth2 应用。 update_oauth2_application_success=您已成功更新了此 OAuth2 应用。
oauth2_application_name=应用名称 oauth2_application_name=应用名称
oauth2_confidential_client=机密客户端。是否是能够维持凭据机密性的应用,比如网页应用程序。如果是本地应用程序请不要勾选,包括桌面和移动端应用。 oauth2_confidential_client=机密客户端。是否是能够维持凭据机密性的应用,比如网页应用程序。如果是本地应用程序请不要勾选,包括桌面和移动端应用。
oauth2_skip_secondary_authorization=首次授权后允许公共客户端跳过授权步骤。 <strong>可能会带来安全风险。</strong>
oauth2_redirect_uris=重定向 URI。每行一个 URI。 oauth2_redirect_uris=重定向 URI。每行一个 URI。
save_application=保存 save_application=保存
oauth2_client_id=客户端ID oauth2_client_id=客户端ID
@ -915,6 +939,7 @@ oauth2_client_secret_hint=您离开或刷新此页面后将不会再显示此密
oauth2_application_edit=编辑 oauth2_application_edit=编辑
oauth2_application_create_description=OAuth2 应用允许您的第三方应用程序访问此实例的用户帐户。 oauth2_application_create_description=OAuth2 应用允许您的第三方应用程序访问此实例的用户帐户。
oauth2_application_remove_description=移除一个OAuth2应用将会阻止它访问此实例上的已授权用户账户。是否继续 oauth2_application_remove_description=移除一个OAuth2应用将会阻止它访问此实例上的已授权用户账户。是否继续
oauth2_application_locked=如果配置启用Gitea预注册一些OAuth2应用程序。 为了防止意外的行为, 这些应用既不能编辑也不能删除。请参阅OAuth2文档以获取更多信息。
authorized_oauth2_applications=已授权的 OAuth2 应用 authorized_oauth2_applications=已授权的 OAuth2 应用
authorized_oauth2_applications_description=您已授予这些第三方应用程序访问您的个人 Gitea 账户的权限。请撤销那些您不再需要的应用程序的访问权限。 authorized_oauth2_applications_description=您已授予这些第三方应用程序访问您的个人 Gitea 账户的权限。请撤销那些您不再需要的应用程序的访问权限。
@ -923,20 +948,26 @@ revoke_oauth2_grant=撤回权限
revoke_oauth2_grant_description=确定撤销此三方应用程序的授权,并阻止此应用程序访问您的数据? revoke_oauth2_grant_description=确定撤销此三方应用程序的授权,并阻止此应用程序访问您的数据?
revoke_oauth2_grant_success=成功撤销了访问权限。 revoke_oauth2_grant_success=成功撤销了访问权限。
twofa_desc=为保护你的账号密码安全你可以使用智能手机或其它设备来接收时间强相关的一次性密码TOTP
twofa_recovery_tip=如果您丢失了您的设备,您将能够使用一次性恢复密钥来重新获得对您账户的访问。 twofa_recovery_tip=如果您丢失了您的设备,您将能够使用一次性恢复密钥来重新获得对您账户的访问。
twofa_is_enrolled=你的账号<strong>已启用</strong>了两步验证。 twofa_is_enrolled=你的账号<strong>已启用</strong>了两步验证。
twofa_not_enrolled=你的账号未开启两步验证。 twofa_not_enrolled=你的账号未开启两步验证。
twofa_disable=禁用两步认证 twofa_disable=禁用两步认证
twofa_scratch_token_regenerate=重新生成初始令牌
twofa_scratch_token_regenerated=您的初始令牌现在是 %s。将其存放在安全的地方它将不会再次显示。
twofa_enroll=启用两步验证 twofa_enroll=启用两步验证
twofa_disable_note=如果需要, 可以禁用双因素身份验证。 twofa_disable_note=如果需要, 可以禁用双因素身份验证。
twofa_disable_desc=关掉两步验证会使得您的账号不安全,继续执行? twofa_disable_desc=关掉两步验证会使得您的账号不安全,继续执行?
regenerate_scratch_token_desc=如果您丢失了您的恢复密钥或已经使用它登录, 您可以在这里重置它。
twofa_disabled=两步验证已被禁用。 twofa_disabled=两步验证已被禁用。
scan_this_image=使用您的授权应用扫描这张图片: scan_this_image=使用您的授权应用扫描这张图片:
or_enter_secret=或者输入密钥:%s or_enter_secret=或者输入密钥:%s
then_enter_passcode=并输入应用程序中显示的密码: then_enter_passcode=并输入应用程序中显示的密码:
passcode_invalid=密码不正确。再试一次。 passcode_invalid=密码不正确。再试一次。
twofa_enrolled=你的账号已经启用了两步验证。请保存初始令牌(%s到一个安全的地方此令牌仅显示一次。
twofa_failed_get_secret=获取 secret 失败。 twofa_failed_get_secret=获取 secret 失败。
webauthn_desc=安全密钥是包含加密密钥的硬件设备。它们可以用于双因素身份验证。安全密钥必须支持 <a rel="noreferrer" target="_blank" href="%s">WebAuthn 身份验证器</a> 标准。
webauthn_register_key=添加安全密钥 webauthn_register_key=添加安全密钥
webauthn_nickname=昵称 webauthn_nickname=昵称
webauthn_delete_key=移除安全密钥 webauthn_delete_key=移除安全密钥
@ -1002,6 +1033,8 @@ fork_to_different_account=派生到其他账号
fork_visibility_helper=无法更改派生仓库的可见性。 fork_visibility_helper=无法更改派生仓库的可见性。
fork_branch=要克隆到 Fork 的分支 fork_branch=要克隆到 Fork 的分支
all_branches=所有分支 all_branches=所有分支
view_all_branches=查看所有分支
view_all_tags=查看所有标签
fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的所有者。 fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的所有者。
fork.blocked_user=无法克隆仓库,因为您被仓库所有者屏蔽。 fork.blocked_user=无法克隆仓库,因为您被仓库所有者屏蔽。
use_template=使用此模板 use_template=使用此模板
@ -1013,6 +1046,8 @@ generate_repo=生成仓库
generate_from=生成自 generate_from=生成自
repo_desc=仓库描述 repo_desc=仓库描述
repo_desc_helper=输入简要描述 (可选) repo_desc_helper=输入简要描述 (可选)
repo_no_desc=无详细信息
repo_lang=编程语言
repo_gitignore_helper=选择 .gitignore 模板。 repo_gitignore_helper=选择 .gitignore 模板。
repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。 repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。
issue_labels=工单标签 issue_labels=工单标签
@ -1074,6 +1109,7 @@ delete_preexisting_success=删除 %s 中未收录的文件
blame_prior=查看此更改前的 blame blame_prior=查看此更改前的 blame
blame.ignore_revs=忽略 <a href="%s">.git-blame-ignore-revs</a> 的修订。点击 <a href="%s">绕过</a> 并查看正常的 Blame 视图。 blame.ignore_revs=忽略 <a href="%s">.git-blame-ignore-revs</a> 的修订。点击 <a href="%s">绕过</a> 并查看正常的 Blame 视图。
blame.ignore_revs.failed=忽略 <a href="%s">.git-blame-ignore-revs</a> 版本失败。 blame.ignore_revs.failed=忽略 <a href="%s">.git-blame-ignore-revs</a> 版本失败。
user_search_tooltip=最多显示30名用户
tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在 tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在
tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。 tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。
@ -1222,6 +1258,7 @@ releases=版本发布
tag=Git标签 tag=Git标签
released_this=发布 released_this=发布
tagged_this=已标记 tagged_this=已标记
file.title=%s 位于 %s
file_raw=原始文件 file_raw=原始文件
file_history=文件历史 file_history=文件历史
file_view_source=源码模式 file_view_source=源码模式
@ -1238,6 +1275,7 @@ ambiguous_runes_header=`此文件含有模棱两可的 Unicode 字符`
ambiguous_runes_description=`此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。` ambiguous_runes_description=`此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。`
invisible_runes_line=`此行含有不可见的 unicode 字符` invisible_runes_line=`此行含有不可见的 unicode 字符`
ambiguous_runes_line=`此行有模棱两可的 unicode 字符` ambiguous_runes_line=`此行有模棱两可的 unicode 字符`
ambiguous_character=`%[1]c [U+%04[1]X] 容易和 %[2]c [U+%04[2]X] 混淆`
escape_control_characters=Escape escape_control_characters=Escape
unescape_control_characters=Unescape unescape_control_characters=Unescape
@ -1429,6 +1467,7 @@ issues.new.clear_milestone=取消选中里程碑
issues.new.assignees=指派成员 issues.new.assignees=指派成员
issues.new.clear_assignees=取消指派成员 issues.new.clear_assignees=取消指派成员
issues.new.no_assignees=未指派成员 issues.new.no_assignees=未指派成员
issues.new.no_reviewers=无审核者
issues.new.blocked_user=无法创建工单,因为您已被仓库所有者屏蔽。 issues.new.blocked_user=无法创建工单,因为您已被仓库所有者屏蔽。
issues.edit.already_changed=无法保存对工单的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改 issues.edit.already_changed=无法保存对工单的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改
issues.edit.blocked_user=无法编辑内容,因为您已被仓库所有者或工单创建者屏蔽。 issues.edit.blocked_user=无法编辑内容,因为您已被仓库所有者或工单创建者屏蔽。
@ -1457,6 +1496,7 @@ issues.remove_labels=于 %[2]s 删除了标签 %[1]s
issues.add_remove_labels=于 %[3]s 添加了标签 %[1]s ,删除了标签 %[2]s issues.add_remove_labels=于 %[3]s 添加了标签 %[1]s ,删除了标签 %[2]s
issues.add_milestone_at=`于 %[2]s 添加了里程碑 <b>%[1]s</b>` issues.add_milestone_at=`于 %[2]s 添加了里程碑 <b>%[1]s</b>`
issues.add_project_at=`于 %[2]s 将此添加到 <b>%[1]s</b> 项目` issues.add_project_at=`于 %[2]s 将此添加到 <b>%[1]s</b> 项目`
issues.move_to_column_of_project=`将此对象移至 %s 的 %s 中在 %s 上`
issues.change_milestone_at=`%[3]s 修改了里程碑从 <b>%[1]s</b> 到 <b>%[2]s</b>` issues.change_milestone_at=`%[3]s 修改了里程碑从 <b>%[1]s</b> 到 <b>%[2]s</b>`
issues.change_project_at=于 %[3]s 将此从项目 <b>%[1]s</b> 移到 <b>%[2]s</b> issues.change_project_at=于 %[3]s 将此从项目 <b>%[1]s</b> 移到 <b>%[2]s</b>
issues.remove_milestone_at=`%[2]s 删除了里程碑 <b>%[1]s</b>` issues.remove_milestone_at=`%[2]s 删除了里程碑 <b>%[1]s</b>`
@ -1488,6 +1528,8 @@ issues.filter_assignee=指派人筛选
issues.filter_assginee_no_select=所有指派成员 issues.filter_assginee_no_select=所有指派成员
issues.filter_assginee_no_assignee=未指派 issues.filter_assginee_no_assignee=未指派
issues.filter_poster=作者 issues.filter_poster=作者
issues.filter_user_placeholder=搜索用户
issues.filter_user_no_select=所有用户
issues.filter_type=类型筛选 issues.filter_type=类型筛选
issues.filter_type.all_issues=所有工单 issues.filter_type.all_issues=所有工单
issues.filter_type.assigned_to_you=指派给您的 issues.filter_type.assigned_to_you=指派给您的
@ -1541,7 +1583,9 @@ issues.no_content=没有提供说明。
issues.close=关闭工单 issues.close=关闭工单
issues.comment_pull_merged_at=已合并提交 %[1]s 到 %[2]s %[3]s issues.comment_pull_merged_at=已合并提交 %[1]s 到 %[2]s %[3]s
issues.comment_manually_pull_merged_at=手动合并提交 %[1]s 到 %[2]s %[3]s issues.comment_manually_pull_merged_at=手动合并提交 %[1]s 到 %[2]s %[3]s
issues.close_comment_issue=评论并关闭
issues.reopen_issue=重新开启 issues.reopen_issue=重新开启
issues.reopen_comment_issue=评论并重新开启
issues.create_comment=评论 issues.create_comment=评论
issues.comment.blocked_user=无法创建或编辑评论,因为您已被仓库所有者或工单创建者屏蔽。 issues.comment.blocked_user=无法创建或编辑评论,因为您已被仓库所有者或工单创建者屏蔽。
issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此工单` issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此工单`
@ -1630,12 +1674,25 @@ issues.delete.title=是否删除工单?
issues.delete.text=您真的要删除这个工单吗?(该操作将会永久删除所有内容。如果您需要保留,请关闭它) issues.delete.text=您真的要删除这个工单吗?(该操作将会永久删除所有内容。如果您需要保留,请关闭它)
issues.tracker=时间跟踪 issues.tracker=时间跟踪
issues.timetracker_timer_start=启动计时器
issues.timetracker_timer_stop=停止计时器
issues.timetracker_timer_discard=删除计时器
issues.timetracker_timer_manually_add=添加时间
issues.time_estimate_set=设置预计时间
issues.time_estimate_display=预计: %s
issues.change_time_estimate_at=将预计时间修改为 <b>%s</b> %s
issues.remove_time_estimate_at=删除预计时间 %s
issues.time_estimate_invalid=预计时间格式无效
issues.start_tracking_history=`开始工作 %s`
issues.tracker_auto_close=当此工单关闭时,自动停止计时器 issues.tracker_auto_close=当此工单关闭时,自动停止计时器
issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!` issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!`
issues.stop_tracking_history=`停止工作 %s`
issues.cancel_tracking_history=`取消时间跟踪 %s` issues.cancel_tracking_history=`取消时间跟踪 %s`
issues.del_time=删除此时间跟踪日志 issues.del_time=删除此时间跟踪日志
issues.add_time_history=`添加计时 %s`
issues.del_time_history=`已删除时间 %s` issues.del_time_history=`已删除时间 %s`
issues.add_time_manually=手动添加时间
issues.add_time_hours=小时 issues.add_time_hours=小时
issues.add_time_minutes=分钟 issues.add_time_minutes=分钟
issues.add_time_sum_to_small=没有输入时间。 issues.add_time_sum_to_small=没有输入时间。
@ -1695,6 +1752,7 @@ issues.dependency.add_error_dep_not_same_repo=这两个工单必须在同一仓
issues.review.self.approval=您不能批准您自己的合并请求。 issues.review.self.approval=您不能批准您自己的合并请求。
issues.review.self.rejection=您不能请求对您自己的合并请求进行更改。 issues.review.self.rejection=您不能请求对您自己的合并请求进行更改。
issues.review.approve=于 %s 批准此合并请求 issues.review.approve=于 %s 批准此合并请求
issues.review.comment=评审于 %s
issues.review.dismissed=于 %[2]s 取消了 %[1]s 的评审 issues.review.dismissed=于 %[2]s 取消了 %[1]s 的评审
issues.review.dismissed_label=已取消 issues.review.dismissed_label=已取消
issues.review.left_comment=留下了一条评论 issues.review.left_comment=留下了一条评论
@ -1720,6 +1778,10 @@ issues.review.resolve_conversation=已解决问题
issues.review.un_resolve_conversation=未解决问题 issues.review.un_resolve_conversation=未解决问题
issues.review.resolved_by=标记问题为已解决 issues.review.resolved_by=标记问题为已解决
issues.review.commented=评论 issues.review.commented=评论
issues.review.official=已批准
issues.review.requested=等待审核
issues.review.rejected=请求变更
issues.review.stale=批准后已更新
issues.review.unofficial=非官方审批数 issues.review.unofficial=非官方审批数
issues.assignee.error=因为未知原因,并非所有的指派都成功。 issues.assignee.error=因为未知原因,并非所有的指派都成功。
issues.reference_issue.body=内容 issues.reference_issue.body=内容
@ -1837,7 +1899,9 @@ pulls.unrelated_histories=合并失败:两个分支没有共同历史。提示
pulls.merge_out_of_date=合并失败:在生成合并时,主分支已更新。提示:再试一次。 pulls.merge_out_of_date=合并失败:在生成合并时,主分支已更新。提示:再试一次。
pulls.head_out_of_date=合并失败在生成合并时head 已更新。提示:再试一次。 pulls.head_out_of_date=合并失败在生成合并时head 已更新。提示:再试一次。
pulls.has_merged=失败:合并请求已经被合并,您不能再次合并或更改目标分支。 pulls.has_merged=失败:合并请求已经被合并,您不能再次合并或更改目标分支。
pulls.push_rejected=推送失败:推送被拒绝。审查此仓库的 Git 钩子。
pulls.push_rejected_summary=详细拒绝信息 pulls.push_rejected_summary=详细拒绝信息
pulls.push_rejected_no_message=推送失败:此推送被拒绝但未提供其他信息。请检查此仓库的 Git 钩子。
pulls.open_unmerged_pull_exists=`您不能执行重新打开操作, 因为已经存在相同的合并请求 (#%d)。` pulls.open_unmerged_pull_exists=`您不能执行重新打开操作, 因为已经存在相同的合并请求 (#%d)。`
pulls.status_checking=一些检测仍在等待运行 pulls.status_checking=一些检测仍在等待运行
pulls.status_checks_success=所有检测均成功 pulls.status_checks_success=所有检测均成功
@ -1881,6 +1945,8 @@ pulls.delete.title=删除此合并请求?
pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它)
pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]s</strong> pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]s</strong>
pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改
pulls.upstream_diverging_merge=同步派生
pull.deleted_branch=(已删除): %s pull.deleted_branch=(已删除): %s
pull.agit_documentation=查看有关 AGit 的文档 pull.agit_documentation=查看有关 AGit 的文档
@ -1894,6 +1960,7 @@ milestones.no_due_date=暂无截止日期
milestones.open=开启 milestones.open=开启
milestones.close=关闭 milestones.close=关闭
milestones.new_subheader=里程碑可以帮助您组织工单并跟踪其进度。 milestones.new_subheader=里程碑可以帮助您组织工单并跟踪其进度。
milestones.completeness=<strong>%d%%</strong> 已完成
milestones.create=创建里程碑 milestones.create=创建里程碑
milestones.title=标题 milestones.title=标题
milestones.desc=描述 milestones.desc=描述
@ -2116,6 +2183,7 @@ settings.pulls.default_delete_branch_after_merge=默认合并后删除合并请
settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者编辑 settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者编辑
settings.releases_desc=启用发布 settings.releases_desc=启用发布
settings.packages_desc=启用仓库软件包注册中心 settings.packages_desc=启用仓库软件包注册中心
settings.projects_desc=启用项目
settings.projects_mode_desc=项目模式 (要显示的项目类型) settings.projects_mode_desc=项目模式 (要显示的项目类型)
settings.projects_mode_repo=仅仓库项目 settings.projects_mode_repo=仅仓库项目
settings.projects_mode_owner=仅限用户或组织项目 settings.projects_mode_owner=仅限用户或组织项目
@ -2155,6 +2223,7 @@ settings.transfer_in_progress=当前正在进行转让。 如果你想将此代
settings.transfer_notices_1=- 如果将此仓库转移给其他用户, 您将失去对此仓库的访问权限。 settings.transfer_notices_1=- 如果将此仓库转移给其他用户, 您将失去对此仓库的访问权限。
settings.transfer_notices_2=-如果将其转移到您 (共同) 拥有的组织,您可以继续访问该仓库。 settings.transfer_notices_2=-如果将其转移到您 (共同) 拥有的组织,您可以继续访问该仓库。
settings.transfer_notices_3=- 如果仓库是私有的并且被转移给某个用户,那么此操作可以确保该用户至少具有读权限(以及必要时的更改权限)。 settings.transfer_notices_3=- 如果仓库是私有的并且被转移给某个用户,那么此操作可以确保该用户至少具有读权限(以及必要时的更改权限)。
settings.transfer_notices_4=- 如果存储库属于某个组织,而您将其转移给另一个组织或个人,那么您将失去存储库工单与其组织项目系统之间的链接。
settings.transfer_owner=新拥有者 settings.transfer_owner=新拥有者
settings.transfer_perform=执行转让 settings.transfer_perform=执行转让
settings.transfer_started=该代码库已被标记为转让并等待来自 %s 的确认 settings.transfer_started=该代码库已被标记为转让并等待来自 %s 的确认
@ -2291,6 +2360,7 @@ settings.event_pull_request_merge=合并请求合并
settings.event_package=软件包 settings.event_package=软件包
settings.event_package_desc=软件包已在仓库中被创建或删除。 settings.event_package_desc=软件包已在仓库中被创建或删除。
settings.branch_filter=分支过滤 settings.branch_filter=分支过滤
settings.branch_filter_desc=推送、创建,删除分支事件的分支白名单,使用 glob 模式匹配指定。若为空或 <code>*</code>,则将报告所有分支的事件。语法文档见 <a href="%[1]s">%[2]s</a>。示例:<code>master</code>,<code>{master,release*}</code>。
settings.authorization_header=授权标头 settings.authorization_header=授权标头
settings.authorization_header_desc=当存在时将被作为授权标头包含在内。例如: %s。 settings.authorization_header_desc=当存在时将被作为授权标头包含在内。例如: %s。
settings.active=激活 settings.active=激活
@ -2336,25 +2406,53 @@ settings.deploy_key_deletion=删除部署密钥
settings.deploy_key_deletion_desc=删除部署密钥将取消此密钥对此仓库的访问权限。继续? settings.deploy_key_deletion_desc=删除部署密钥将取消此密钥对此仓库的访问权限。继续?
settings.deploy_key_deletion_success=部署密钥已删除。 settings.deploy_key_deletion_success=部署密钥已删除。
settings.branches=分支 settings.branches=分支
settings.protected_branch=分支保护
settings.protected_branch.save_rule=保存规则 settings.protected_branch.save_rule=保存规则
settings.protected_branch.delete_rule=删除规则 settings.protected_branch.delete_rule=删除规则
settings.protected_branch_can_push=是否允许推送?
settings.protected_branch_can_push_yes=你可以推 settings.protected_branch_can_push_yes=你可以推
settings.protected_branch_can_push_no=你不能推送
settings.branch_protection=分支 '<b>%s</b>' 的保护规则
settings.protect_this_branch=启用分支保护 settings.protect_this_branch=启用分支保护
settings.protect_this_branch_desc=阻止删除并限制Git推送和合并到分支。 settings.protect_this_branch_desc=阻止删除并限制Git推送和合并到分支。
settings.protect_disable_push=禁用推送 settings.protect_disable_push=禁用推送
settings.protect_disable_push_desc=此分支不允许推送。 settings.protect_disable_push_desc=此分支不允许推送。
settings.protect_disable_force_push=禁用强制推送
settings.protect_disable_force_push_desc=此分支禁止强制推送。
settings.protect_enable_push=启用推送 settings.protect_enable_push=启用推送
settings.protect_enable_push_desc=任何拥有写访问权限的人将被允许推送到此分支(但不能强行推送)。 settings.protect_enable_push_desc=任何拥有写访问权限的人将被允许推送到此分支(但不能强行推送)。
settings.protect_enable_force_push_all=启用强制推送
settings.protect_enable_force_push_all_desc=任何拥有推送权限的人都可以对这个分支进行强制推送。
settings.protect_enable_force_push_allowlist=强制推送白名单
settings.protect_enable_force_push_allowlist_desc=只有白名单中的用户或团队才能强制推送到这个分支。
settings.protect_enable_merge=启用合并 settings.protect_enable_merge=启用合并
settings.protect_enable_merge_desc=任何具有写入权限的人都可以将合并请求合并到此分支中。 settings.protect_enable_merge_desc=任何具有写入权限的人都可以将合并请求合并到此分支中。
settings.protect_whitelist_committers=受白名单限制的推送
settings.protect_whitelist_committers_desc=只有列入白名单的用户或团队才能被允许推送到此分支(但不能强行推送)。
settings.protect_whitelist_deploy_keys=具有推送权限的部署密钥白名单。
settings.protect_whitelist_users=推送白名单用户:
settings.protect_whitelist_teams=推送白名单团队:
settings.protect_force_push_allowlist_users=允许强制推送的白名单用户:
settings.protect_force_push_allowlist_teams=允许强制推送的白名单团队:
settings.protect_force_push_allowlist_deploy_keys=允许白名单中的部署密钥进行强制推送。
settings.protect_merge_whitelist_committers=启用合并白名单
settings.protect_merge_whitelist_committers_desc=仅允许白名单用户或团队合并合并请求到此分支。
settings.protect_merge_whitelist_users=合并白名单用户:
settings.protect_merge_whitelist_teams=合并白名单团队:
settings.protect_check_status_contexts=启用状态检查 settings.protect_check_status_contexts=启用状态检查
settings.protect_status_check_patterns=状态检查模式: settings.protect_status_check_patterns=状态检查模式:
settings.protect_status_check_patterns_desc=输入模式,指定哪些状态检查必须通过,才能将分支合并到符合此规则的分支中去。每一行指定一个模式,模式不能为空。 settings.protect_status_check_patterns_desc=输入模式,指定哪些状态检查必须通过,才能将分支合并到符合此规则的分支中去。每一行指定一个模式,模式不能为空。
settings.protect_check_status_contexts_desc=要求状态检查通过才能合并。如果启用,提交必须先推送到另一个分支,然后再合并或推送到匹配这些保护规则的分支。如果没有选择具体的状态检查上下文,则所有的状态检查都通过才能合并。
settings.protect_check_status_contexts_list=此仓库上周进行过的状态检查 settings.protect_check_status_contexts_list=此仓库上周进行过的状态检查
settings.protect_status_check_matched=匹配 settings.protect_status_check_matched=匹配
settings.protect_invalid_status_check_pattern=无效的状态检查规则:“%s”。 settings.protect_invalid_status_check_pattern=无效的状态检查规则:“%s”。
settings.protect_no_valid_status_check_patterns=没有有效的状态检查规则。 settings.protect_no_valid_status_check_patterns=没有有效的状态检查规则。
settings.protect_required_approvals=所需的批准: settings.protect_required_approvals=所需的批准:
settings.protect_required_approvals_desc=只允许合并有足够审核的合并请求。要求的审核必须来自白名单或者有权限的用户或团队。
settings.protect_approvals_whitelist_enabled=仅列入白名单的用户或团队才可批准
settings.protect_approvals_whitelist_enabled_desc=只有白名单用户或团队的审核才能计数。 如果规则没有批准白名单,来自任何有写访问权限的人的审核都将计数。
settings.protect_approvals_whitelist_users=审查者白名单:
settings.protect_approvals_whitelist_teams=审查团队白名单:
settings.dismiss_stale_approvals=取消过时的批准 settings.dismiss_stale_approvals=取消过时的批准
settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。 settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。
settings.ignore_stale_approvals=忽略过期批准 settings.ignore_stale_approvals=忽略过期批准
@ -2362,12 +2460,18 @@ settings.ignore_stale_approvals_desc=对旧提交(过期审核)的批准将
settings.require_signed_commits=需要签名提交 settings.require_signed_commits=需要签名提交
settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支 settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支
settings.protect_branch_name_pattern=受保护的分支名称模式 settings.protect_branch_name_pattern=受保护的分支名称模式
settings.protect_branch_name_pattern_desc=分支保护的名称匹配规则。语法请参阅 <a href="%s">文档</a> 。如main, release/**
settings.protect_patterns=规则 settings.protect_patterns=规则
settings.protect_protected_file_patterns=受保护的文件模式(使用分号 ';' 分隔) settings.protect_protected_file_patterns=受保护的文件模式(使用分号 ';' 分隔)
settings.protect_protected_file_patterns_desc=即使用户有权添加、编辑或删除此分支中的文件,也不允许直接更改受保护的文件。 可以使用分号 (';') 分隔多个模式。 见<a href='%[1]s'>%[2]s</a>文档了解模式语法。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>
settings.protect_unprotected_file_patterns=不受保护的文件模式(使用分号 ';' 分隔) settings.protect_unprotected_file_patterns=不受保护的文件模式(使用分号 ';' 分隔)
settings.protect_unprotected_file_patterns_desc=如果用户有写权限,则允许直接更改的不受保护的文件,以绕过推送限制。可以使用分号分隔多个模式 (';')。 见 <a href='%[1]s'>%[2]s</a> 文档了解模式语法。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>
settings.add_protected_branch=启用保护
settings.delete_protected_branch=禁用保护
settings.update_protect_branch_success=分支保护规则 %s 更新成功。 settings.update_protect_branch_success=分支保护规则 %s 更新成功。
settings.remove_protected_branch_success=移除分支保护规则"%s"成功。 settings.remove_protected_branch_success=移除分支保护规则"%s"成功。
settings.remove_protected_branch_failed=移除分支保护规则"%s"失败。 settings.remove_protected_branch_failed=移除分支保护规则"%s"失败。
settings.protected_branch_deletion=删除分支保护
settings.protected_branch_deletion_desc=禁用分支保护允许具有写入权限的用户推送提交到此分支。继续? settings.protected_branch_deletion_desc=禁用分支保护允许具有写入权限的用户推送提交到此分支。继续?
settings.block_rejected_reviews=拒绝审核阻止了此合并 settings.block_rejected_reviews=拒绝审核阻止了此合并
settings.block_rejected_reviews_desc=如果官方审查人员要求作出改动,即使有足够的批准,合并也不允许。 settings.block_rejected_reviews_desc=如果官方审查人员要求作出改动,即使有足够的批准,合并也不允许。
@ -2375,8 +2479,11 @@ settings.block_on_official_review_requests=有官方审核阻止了代码合并
settings.block_on_official_review_requests_desc=处于评审状态时,即使有足够的批准,也不能合并。 settings.block_on_official_review_requests_desc=处于评审状态时,即使有足够的批准,也不能合并。
settings.block_outdated_branch=如果合并请求已经过时,阻止合并 settings.block_outdated_branch=如果合并请求已经过时,阻止合并
settings.block_outdated_branch_desc=当头部分支落后基础分支时,不能合并。 settings.block_outdated_branch_desc=当头部分支落后基础分支时,不能合并。
settings.block_admin_merge_override=管理员须遵守分支保护规则
settings.block_admin_merge_override_desc=管理员须遵守分支保护规则,不能规避该规则。
settings.default_branch_desc=请选择一个默认的分支用于合并请求和提交: settings.default_branch_desc=请选择一个默认的分支用于合并请求和提交:
settings.merge_style_desc=合并方式 settings.merge_style_desc=合并方式
settings.default_merge_style_desc=默认合并风格
settings.choose_branch=选择一个分支... settings.choose_branch=选择一个分支...
settings.no_protected_branch=没有受保护的分支 settings.no_protected_branch=没有受保护的分支
settings.edit_protected_branch=编辑 settings.edit_protected_branch=编辑
@ -2392,12 +2499,25 @@ settings.tags.protection.allowed.teams=允许的团队
settings.tags.protection.allowed.noone= settings.tags.protection.allowed.noone=
settings.tags.protection.create=保护Git标签 settings.tags.protection.create=保护Git标签
settings.tags.protection.none=没有受保护的Git标签 settings.tags.protection.none=没有受保护的Git标签
settings.tags.protection.pattern.description=你可以使用单个名称或 glob 模式匹配或正则表达式来匹配多个标签。了解详情请访问 <a target="_blank" rel="noopener" href="%s">保护Git标签指南</a>。
settings.bot_token=Bot 令牌 settings.bot_token=Bot 令牌
settings.chat_id=聊天 ID settings.chat_id=聊天 ID
settings.thread_id=线程 ID settings.thread_id=线程 ID
settings.matrix.homeserver_url=主服务器网址 settings.matrix.homeserver_url=主服务器网址
settings.matrix.room_id=房间ID settings.matrix.room_id=房间ID
settings.matrix.message_type=消息类型 settings.matrix.message_type=消息类型
settings.visibility.private.button=设为私有
settings.visibility.private.text=将可见性更改为私有不仅会使仓库仅对允许的成员可见,而且可能会消除它与派生仓库、关注者和点赞之间的关系。
settings.visibility.private.bullet_title=<strong>将可见性改为私有将会:</strong>
settings.visibility.private.bullet_one=使仓库只对允许的成员可见。
settings.visibility.private.bullet_two=可能会删除它与 <strong>派生仓库</strong>、 <strong>关注者</strong>和 <strong>点赞</strong> 之间的关系。
settings.visibility.public.button=设为公开
settings.visibility.public.text=将可见性更改为公开会使任何人都可见。
settings.visibility.public.bullet_title=<strong>将可见性改为公开将会:</strong>
settings.visibility.public.bullet_one=使仓库让任何人都可见。
settings.visibility.success=仓库可见性已更改。
settings.visibility.error=试图更改仓库可见性时出错。
settings.visibility.fork_error=无法更改派生仓库的可见性。
settings.archive.button=归档仓库 settings.archive.button=归档仓库
settings.archive.header=归档此仓库 settings.archive.header=归档此仓库
settings.archive.text=归档仓库将使其完全只读。它将在首页隐藏。没有人(甚至你!)能够进行新的提交,或打开工单及合并请求。 settings.archive.text=归档仓库将使其完全只读。它将在首页隐藏。没有人(甚至你!)能够进行新的提交,或打开工单及合并请求。
@ -2509,6 +2629,7 @@ release.new_release=发布新版
release.draft=草稿 release.draft=草稿
release.prerelease=预发行 release.prerelease=预发行
release.stable=稳定 release.stable=稳定
release.latest=最新版本
release.compare=比较 release.compare=比较
release.edit=编辑 release.edit=编辑
release.ahead.commits=<strong>%d</strong> 次提交 release.ahead.commits=<strong>%d</strong> 次提交
@ -2593,6 +2714,7 @@ tag.create_success=标签"%s"已存在
topic.manage_topics=管理主题 topic.manage_topics=管理主题
topic.done=保存 topic.done=保存
topic.count_prompt=您最多选择25个主题
topic.format_prompt=主题必须以字母或数字开头,可以包含连字符 ('-') 和句点 ('.')长度不得超过35个字符。字符必须为小写。 topic.format_prompt=主题必须以字母或数字开头,可以包含连字符 ('-') 和句点 ('.')长度不得超过35个字符。字符必须为小写。
find_file.go_to_file=转到文件 find_file.go_to_file=转到文件
@ -2665,6 +2787,7 @@ settings.delete_prompt=删除操作会永久清除该组织的信息,并且 <s
settings.confirm_delete_account=确认删除组织 settings.confirm_delete_account=确认删除组织
settings.delete_org_title=删除组织 settings.delete_org_title=删除组织
settings.delete_org_desc=此组织将会被永久删除,确认继续吗? settings.delete_org_desc=此组织将会被永久删除,确认继续吗?
settings.hooks_desc=在此处添加的 Web 钩子将会应用到该组织下的 <strong>所有仓库</strong>。
settings.labels_desc=添加能够被该组织下的 <strong>所有仓库</strong> 的工单使用的标签。 settings.labels_desc=添加能够被该组织下的 <strong>所有仓库</strong> 的工单使用的标签。
@ -2689,6 +2812,7 @@ teams.leave.detail=离开 %s
teams.can_create_org_repo=创建仓库 teams.can_create_org_repo=创建仓库
teams.can_create_org_repo_helper=成员可以在组织中创建仓库。创建者将自动获得创建的仓库的管理员权限。 teams.can_create_org_repo_helper=成员可以在组织中创建仓库。创建者将自动获得创建的仓库的管理员权限。
teams.none_access=无访问权限 teams.none_access=无访问权限
teams.none_access_helper=成员无法查看此单元或对其执行任何其他操作。对公开仓库无此影响。
teams.general_access=常规访问 teams.general_access=常规访问
teams.general_access_helper=成员权限将由以下权限表决定。 teams.general_access_helper=成员权限将由以下权限表决定。
teams.read_access=可读 teams.read_access=可读
@ -2757,6 +2881,7 @@ last_page=末页
total=总计:%d total=总计:%d
settings=管理设置 settings=管理设置
dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="%s">博客</a> 了解详情。
dashboard.statistic=摘要 dashboard.statistic=摘要
dashboard.maintenance_operations=运维 dashboard.maintenance_operations=运维
dashboard.system_status=系统状态 dashboard.system_status=系统状态
@ -2799,6 +2924,7 @@ dashboard.reinit_missing_repos=重新初始化所有丢失的 Git 仓库存在
dashboard.sync_external_users=同步外部用户数据 dashboard.sync_external_users=同步外部用户数据
dashboard.cleanup_hook_task_table=清理 hook_task 表 dashboard.cleanup_hook_task_table=清理 hook_task 表
dashboard.cleanup_packages=清理过期的软件包 dashboard.cleanup_packages=清理过期的软件包
dashboard.cleanup_actions=清理过期的 Actions 资源
dashboard.server_uptime=服务运行时间 dashboard.server_uptime=服务运行时间
dashboard.current_goroutine=当前 Goroutines 数量 dashboard.current_goroutine=当前 Goroutines 数量
dashboard.current_memory_usage=当前内存使用量 dashboard.current_memory_usage=当前内存使用量
@ -2828,12 +2954,19 @@ dashboard.total_gc_time=GC 暂停时间总量
dashboard.total_gc_pause=GC 暂停时间总量 dashboard.total_gc_pause=GC 暂停时间总量
dashboard.last_gc_pause=上次 GC 暂停时间 dashboard.last_gc_pause=上次 GC 暂停时间
dashboard.gc_times=GC 执行次数 dashboard.gc_times=GC 执行次数
dashboard.delete_old_actions=从数据库中删除所有旧操作记录
dashboard.delete_old_actions.started=已开始从数据库中删除所有旧操作记录。
dashboard.update_checker=更新检查器 dashboard.update_checker=更新检查器
dashboard.delete_old_system_notices=从数据库中删除所有旧系统通知 dashboard.delete_old_system_notices=从数据库中删除所有旧系统通知
dashboard.gc_lfs=垃圾回收 LFS 元数据 dashboard.gc_lfs=垃圾回收 LFS 元数据
dashboard.stop_zombie_tasks=停止僵尸任务
dashboard.stop_endless_tasks=停止无法停止的任务
dashboard.cancel_abandoned_jobs=取消丢弃的任务
dashboard.start_schedule_tasks=开始Actions调度任务
dashboard.sync_branch.started=分支同步已开始 dashboard.sync_branch.started=分支同步已开始
dashboard.sync_tag.started=标签同步已开始 dashboard.sync_tag.started=标签同步已开始
dashboard.rebuild_issue_indexer=重建工单索引 dashboard.rebuild_issue_indexer=重建工单索引
dashboard.sync_repo_licenses=重新仓库许可证探测
users.user_manage_panel=用户帐户管理 users.user_manage_panel=用户帐户管理
users.new_account=创建新帐户 users.new_account=创建新帐户
@ -2905,6 +3038,10 @@ emails.not_updated=无法更新请求的电子邮件地址: %v
emails.duplicate_active=此电子邮件地址已被另一个用户激活使用。 emails.duplicate_active=此电子邮件地址已被另一个用户激活使用。
emails.change_email_header=更新电子邮件属性 emails.change_email_header=更新电子邮件属性
emails.change_email_text=您确定要更新该电子邮件地址吗? emails.change_email_text=您确定要更新该电子邮件地址吗?
emails.delete=删除电子邮件
emails.delete_desc=您确定要删除该电子邮件地址?
emails.deletion_success=电子邮件地址已被删除。
emails.delete_primary_email_error=您不能删除主电子邮件。
orgs.org_manage_panel=组织管理 orgs.org_manage_panel=组织管理
orgs.name=名称 orgs.name=名称
@ -2937,10 +3074,12 @@ packages.size=大小
packages.published=已发布 packages.published=已发布
defaulthooks=默认Web钩子 defaulthooks=默认Web钩子
defaulthooks.desc=当某些 Gitea 事件触发时Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置,将被复制到所有新的仓库中。详情请访问 <a target="_blank" rel="noopener" href="%s">Web 钩子指南</a>。
defaulthooks.add_webhook=添加默认Web 钩子 defaulthooks.add_webhook=添加默认Web 钩子
defaulthooks.update_webhook=更新默认Web钩子 defaulthooks.update_webhook=更新默认Web钩子
systemhooks=系统 Web 钩子 systemhooks=系统 Web 钩子
systemhooks.desc=当某些 Gitea 事件触发时Web 钩子自动向服务器发出HTTP POST请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。了解详情请访问 <a target="_blank" rel="noopener" href="%s">Web 钩子指南</a>。
systemhooks.add_webhook=添加系统 Web 钩子 systemhooks.add_webhook=添加系统 Web 钩子
systemhooks.update_webhook=更新系统 Web 钩子 systemhooks.update_webhook=更新系统 Web 钩子
@ -3035,8 +3174,18 @@ auths.tips=帮助提示
auths.tips.oauth2.general=OAuth2 认证 auths.tips.oauth2.general=OAuth2 认证
auths.tips.oauth2.general.tip=当注册新的 OAuth2 身份验证时,回调/重定向 URL 应该是: auths.tips.oauth2.general.tip=当注册新的 OAuth2 身份验证时,回调/重定向 URL 应该是:
auths.tip.oauth2_provider=OAuth2 提供程序 auths.tip.oauth2_provider=OAuth2 提供程序
auths.tip.bitbucket=在 %s 注册新的 OAuth 使用者同时添加权限“账号”-“读取”
auths.tip.nextcloud=使用下面的菜单“设置Settings -> 安全Security -> OAuth 2.0 client”在您的实例上注册一个新的 OAuth 客户端。 auths.tip.nextcloud=使用下面的菜单“设置Settings -> 安全Security -> OAuth 2.0 client”在您的实例上注册一个新的 OAuth 客户端。
auths.tip.dropbox=在 %s 上创建一个新的应用程序
auths.tip.facebook=`在 %s 注册一个新的应用,并添加产品"Facebook 登录"`
auths.tip.github=在 %s 注册一个 OAuth 应用程序
auths.tip.gitlab_new=在 %s 注册一个新的应用
auths.tip.google_plus=从谷歌 API 控制台 %s 获得 OAuth2 客户端凭据
auths.tip.openid_connect=使用 OpenID 连接发现 URL ({server}/.well-known/openid-configuration) 来指定终点 auths.tip.openid_connect=使用 OpenID 连接发现 URL ({server}/.well-known/openid-configuration) 来指定终点
auths.tip.twitter=访问 %s创建应用并确保启用了"允许此应用程序用于登录 Twitter"的选项。
auths.tip.discord=在 %s 上注册新应用程序
auths.tip.gitea=注册一个新的 OAuth2 应用程序。可以访问 %s 查看帮助
auths.tip.yandex=在 %s 上创建一个新的应用程序。在“ Yandex.Passport API”这部分中选择以下权限“访问电子邮件地址Access to email address“访问用户头像Access to user avatar”和“访问用户名名字和姓氏性别Access to username, first name and surname, genderAccess to username, first name and surname, gender
auths.tip.mastodon=输入您想要认证的 mastodon 实例的自定义 URL (或使用默认值) auths.tip.mastodon=输入您想要认证的 mastodon 实例的自定义 URL (或使用默认值)
auths.edit=修改认证源 auths.edit=修改认证源
auths.activated=该认证源已经启用 auths.activated=该认证源已经启用
@ -3153,6 +3302,9 @@ config.cache_interval=Cache 周期
config.cache_conn=Cache 连接字符串 config.cache_conn=Cache 连接字符串
config.cache_item_ttl=缓存项目 TTL config.cache_item_ttl=缓存项目 TTL
config.cache_test=测试缓存 config.cache_test=测试缓存
config.cache_test_failed=缓存测试失败: %v。
config.cache_test_slow=缓存测试成功,但响应缓慢: %s。
config.cache_test_succeeded=缓存测试成功,在 %s 时间内得到响应。
config.session_config=Session 配置 config.session_config=Session 配置
config.session_provider=Session 提供者 config.session_provider=Session 提供者
@ -3199,6 +3351,7 @@ monitor.next=下次执行时间
monitor.previous=上次执行时间 monitor.previous=上次执行时间
monitor.execute_times=执行次数 monitor.execute_times=执行次数
monitor.process=运行中进程 monitor.process=运行中进程
monitor.stacktrace=调用栈踪迹
monitor.processes_count=%d 个进程 monitor.processes_count=%d 个进程
monitor.download_diagnosis_report=下载诊断报告 monitor.download_diagnosis_report=下载诊断报告
monitor.desc=进程描述 monitor.desc=进程描述
@ -3206,6 +3359,8 @@ monitor.start=开始时间
monitor.execute_time=执行时长 monitor.execute_time=执行时长
monitor.last_execution_result=结果 monitor.last_execution_result=结果
monitor.process.cancel=中止进程 monitor.process.cancel=中止进程
monitor.process.cancel_desc=中止一个进程可能导致数据丢失
monitor.process.cancel_notices=中止:<strong>%s</strong>
monitor.process.children=子进程 monitor.process.children=子进程
monitor.queues=队列 monitor.queues=队列
@ -3307,6 +3462,8 @@ raw_minutes=分钟
[dropzone] [dropzone]
default_message=拖动文件或者点击此处上传。 default_message=拖动文件或者点击此处上传。
invalid_input_type=您不能上传该类型的文件
file_too_big=文件体积({{filesize}} MB超过了最大允许体积{{maxFilesize}} MB
remove_file=移除文件 remove_file=移除文件
[notification] [notification]
@ -3378,6 +3535,8 @@ alpine.repository=仓库信息
alpine.repository.branches=分支 alpine.repository.branches=分支
alpine.repository.repositories=仓库管理 alpine.repository.repositories=仓库管理
alpine.repository.architectures=架构 alpine.repository.architectures=架构
arch.registry=添加具有相关仓库和架构的服务器到 <code>/etc/pacman.conf</code> 中:
arch.install=使用 pacman 同步软件包:
arch.repository=仓库信息 arch.repository=仓库信息
arch.repository.repositories=仓库管理 arch.repository.repositories=仓库管理
arch.repository.architectures=架构 arch.repository.architectures=架构
@ -3456,6 +3615,7 @@ settings.link=将此软件包链接到仓库
settings.link.description=如果您将一个软件包与一个代码库链接起来,软件包将显示在代码库的软件包列表中。 settings.link.description=如果您将一个软件包与一个代码库链接起来,软件包将显示在代码库的软件包列表中。
settings.link.select=选择仓库 settings.link.select=选择仓库
settings.link.button=更新仓库链接 settings.link.button=更新仓库链接
settings.link.success=仓库链接已成功更新。
settings.link.error=更新仓库链接失败。 settings.link.error=更新仓库链接失败。
settings.delete=删除软件包 settings.delete=删除软件包
settings.delete.description=删除软件包是永久性的,无法撤消。 settings.delete.description=删除软件包是永久性的,无法撤消。
@ -3579,12 +3739,18 @@ runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗请查看
runs.no_workflows.documentation=关于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.no_runs=工作流尚未运行过。
runs.empty_commit_message=(空白的提交消息) runs.empty_commit_message=(空白的提交消息)
runs.expire_log_message=旧的日志已被清除
workflow.disable=禁用工作流 workflow.disable=禁用工作流
workflow.disable_success=工作流 '%s' 已成功禁用。 workflow.disable_success=工作流 '%s' 已成功禁用。
workflow.enable=启用工作流 workflow.enable=启用工作流
workflow.enable_success=工作流 '%s' 已成功启用。 workflow.enable_success=工作流 '%s' 已成功启用。
workflow.disabled=工作流已禁用。 workflow.disabled=工作流已禁用。
workflow.run=运行工作流
workflow.not_found=工作流 %s 未找到。
workflow.run_success=工作流 %s 已成功运行。
workflow.from_ref=使用工作流从
workflow.has_workflow_dispatch=此 Workflow 有一个 Workflow_dispatch 事件触发器。
need_approval_desc=该工作流由派生仓库的合并请求所触发,需要批准方可运行。 need_approval_desc=该工作流由派生仓库的合并请求所触发,需要批准方可运行。
@ -3605,6 +3771,7 @@ variables.update.failed=编辑变量失败。
variables.update.success=该变量已被编辑。 variables.update.success=该变量已被编辑。
[projects] [projects]
deleted.display_name=已删除项目
type-1.display_name=个人项目 type-1.display_name=个人项目
type-2.display_name=仓库项目 type-2.display_name=仓库项目
type-3.display_name=组织项目 type-3.display_name=组织项目

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